Подтвердить что ты не робот

Почему я не могу наследовать от int в С++?

Мне бы очень хотелось это сделать:

class myInt : public int
{

};

Почему я не могу?

Зачем мне это нужно? Более сильный ввод. Например, я мог бы определить два класса intA и intB, которые позволяют мне делать intA + intA или intB + intB, но не intA + intB.

"Инты - это не классы". Итак, что?

"У Ints нет данных участника". Да, они имеют 32 бита или что-то еще.

"Инты не имеют каких-либо функций-членов". Ну, у них есть целая группа операторов вроде + и -.

4b9b3361

Ответ 1

Нил комментирует довольно точно. Бьярне упомянул, рассматривая и отвергая эту точную возможность 1:

Синтаксис инициализатора раньше был недопустимым для встроенных типов. Чтобы позволить это, я ввел понятие, что встроенные типы имеют конструкторы и деструкторы. Например:

int a(1);    // pre-2.1 error, now initializes a to 1

Я подумал о расширении этого понятия, чтобы разрешить вывод из встроенных классов и явное объявление встроенных операторов для встроенных типов. Тем не менее, я сдержался.

Разрешение наследования от int фактически не дает программисту C++ ничего нового по сравнению с наличием члена int. Это в первую очередь потому, что int не имеет виртуальных функций для переопределения производного класса. Более серьезно, однако, правила преобразования C настолько хаотичны, что притворяться, что int, short и т.д. - это обычные классы с хорошим поведением, не сработает. Они либо совместимы с C, либо подчиняются правилам C++ относительно хорошего поведения для классов, но не обоим.

Что касается комментария, то производительность оправдывает то, что он не делает int классом, он (по крайней мере, в основном) ложный. В Smalltalk все типы являются классами, но почти во всех реализациях Smalltalk есть оптимизации, поэтому реализация может быть практически идентична тому, как вы работали бы с неклассовыми типами. Например, класс smallInteger представляет 15-разрядное целое число, а сообщение "+" жестко запрограммировано в виртуальной машине, поэтому даже если вы можете получить из smallInteger, оно все равно дает производительность, аналогичную встроенному типу ( хотя Smalltalk достаточно отличается от C++, что прямые сравнения производительности трудны и вряд ли будут много значить).

Один бит, который "теряется" в реализации SmallInteger для Smalltalk (причина, по которой он представляет только 15 бит вместо 16), вероятно, не понадобится в C или C++. Smalltalk немного похож на Java - когда вы "определяете объект", вы на самом деле просто определяете указатель на объект, и вам нужно динамически выделить объект, на который он будет указывать. То, что вы манипулируете, передаете функции в качестве параметра и т.д., Всегда является просто указателем, а не самим объектом.

Это не то, как smallInteger реализован, хотя - в его случае, они помещают целочисленное значение непосредственно в то, что обычно будет указателем. Чтобы различать smallInteger и указатель, они заставляют все объекты размещаться на границах четных байтов, поэтому LSB всегда чист. У smallInteger всегда установлен LSB.

Однако большая часть этого необходима, потому что Smalltalk динамически типизирован - он должен иметь возможность определять тип, просматривая само значение, а smallInteger в основном использует этот LSB в качестве тега типа. Учитывая, что C++ статически типизирован, нет необходимости выводить тип из значения, поэтому вам, вероятно, не нужно будет "тратить" этот бит на тег типа.


1. В Проекте и Развитии C++, §15.11.3.

Ответ 2

Int - это порядковый тип, а не класс. Зачем вам это нужно?

Если вам нужно добавить функциональность в "int", подумайте о создании класса агрегата, который имеет целочисленное поле, и методов, которые раскрывают любые дополнительные возможности, которые вам нужны.

Обновление

@OP "Инты не являются классами" так?

Наследование, полиморфизм и инкапсуляция являются ключевыми объектами объектно-ориентированного дизайна. Ни одна из этих вещей не относится к порядковым типам. Вы не можете наследовать от int, потому что это всего лишь куча байтов и не имеет кода.

Инты, символы и другие порядковые типы не имеют таблиц методов, поэтому нет способа добавлять методы или переопределять их, что на самом деле является сердцем наследования.

Ответ 3

Зачем мне это нужно? Более сильный ввод. Например, я мог бы определить два класса intA и intB, которые позволяют мне делать intA + intA или intB + intB, но не intA + intB.

Это не имеет смысла. Вы можете делать все это, не наследуя ни от чего. (И, с другой стороны, я не вижу, как вы могли бы достичь этого, используя наследование.) Например,

class SpecialInt {
 ...
};
SpecialInt operator+ (const SpecialInt& lhs, const SpecialInt& rhs) {
  ...
}

Заполните пробелы, и у вас есть тип, который решает вашу проблему. Вы можете сделать SpecialInt + SpecialInt или int + int, но SpecialInt + int не будет компилироваться точно так, как вы хотели.

С другой стороны, если мы притворились, что наследование из int является законным, а наш SpecialInt, полученный из int, тогда SpecialInt + int будет компилироваться. Наследование приведет к точной проблеме, которую вы хотите избежать. Не наследование позволяет легко устранить проблему.

"Инты не имеют каких-либо функций-членов". Ну, у них есть целая группа операторов вроде + и -.

Однако это не функции-члены.

Ответ 4

Если OP действительно хочет понять, ПОЧЕМУ С++ - так оно и есть, тогда он должен получить копию книги Stroustup "Дизайн и эволюция С++". Это объясняет обоснование этого и многих других дизайнерских решений в первые дни С++.

Ответ 5

Поскольку int является нативным типом, а не классом

Изменить: перевод моих комментариев в мой ответ.

Это происходит из наследия C и что, собственно, представляют примитивы. Примитив в С++ - это просто набор байтов, которые имеют мало смысла, кроме компилятора. С другой стороны, класс имеет таблицу функций, и как только вы начнете спускать путь наследования и виртуального наследования, у вас есть таблица vtable. Ничто из этого не присутствует в примитиве, и, представив его, вы: a) сложите много c-кода, который предполагает, что int имеет только 8 байтов, и b) делает программы занимают намного больше памяти.

Подумайте об этом по-другому. int/float/ char не имеют каких-либо элементов данных или методов. Подумайте о примитивах как кварках - они являются строительными блоками, которые вы не можете разделить, вы используете их, чтобы делать большие вещи (извинения, если моя аналогия немного неактуальна, я не знаю достаточной физики частиц)

Ответ 6

строгая типизация целых чисел (и чисел с плавающей точкой) в c++

Скотт Мейер (Effective c++ имеет очень эффективное и мощное решение вашей проблемы строгой типизации базовых типов в c++, и оно работает так:

Строгая типизация - это проблема, которую можно решить и оценить во время компиляции. Это означает, что вы можете использовать порядковые номера (слабая типизация) для нескольких типов во время выполнения в развернутых приложениях и использовать специальную фазу компиляции, чтобы сгладить неподходящие комбинации типов. во время компиляции.

#ifdef STRONG_TYPE_COMPILE
typedef time Time
typedef distance Distance
typedef velocity Velocity
#else
typedef time float
typedef distance float
typedef velocity float
#endif

Затем вы определяете свое Time, Mass и Distance как классы со всеми (и только) соответствующими операторами, перегруженными для соответствующих операций. В псевдокоде:

class Time {
  public: 
  float value;
  Time operator +(Time b) {self.value + b.value;}
  Time operator -(Time b) {self.value - b.value;}
  // don't define Time*Time, Time/Time etc.
  Time operator *(float b) {self.value * b;}
  Time operator /(float b) {self.value / b;}
}

class Distance {
  public:
  float value;
  Distance operator +(Distance b) {self.value + b.value;}
  // also -, but not * or /
  Velocity operator /(Time b) {Velocity( self.value / b.value )}
}

class Velocity {
  public:
  float value;
  // appropriate operators
  Velocity(float a) : value(a) {}
}

Как только это будет сделано, ваш компилятор сообщит вам о любых местах, где вы нарушили правила, закодированные в вышеуказанных классах.

Я позволю вам проработать остальные детали самостоятельно или купить книгу.

Ответ 7

То, что говорили другие, истинно... int - это примитив в С++ (как С#). Однако вы можете достичь того, чего хотите, просто построив класс вокруг int:

class MyInt
{
private:
   int mInt;

public:
   explicit MyInt(int in) { mInt = in; }
   // Getters/setters etc
};

Затем вы можете наследовать от всего, что вам нужно.

Ответ 8

Никто не упомянул, что С++ был разработан, чтобы иметь (в основном) обратную совместимость с C, чтобы облегчить путь обновления для C-кодеров, следовательно struct по умолчанию для всех участников и т.д.

Наличие int в качестве базового класса, который вы могли бы переопределить, в корне усложняло бы это правило без конца и сделало бы реализацию компилятора адским, которая, если вы хотите, чтобы существующие кодеры и поставщики компиляторов поддерживали ваш неоперившийся язык, вероятно, не стоили усилий.

Ответ 9

В С++ встроенные типы не являются классами.

Ответ 10

Как я уже говорил, это невозможно, так как int является примитивным типом.

Я понимаю мотивацию, хотя, если это для более сильного набора текста. Для С++ 0x даже было предложено, чтобы для этого было достаточно специального типа typedef (но это было отклонено?).

Возможно, что-то может быть достигнуто, если вы предоставили базовую оболочку самостоятельно. Например, что-то вроде следующего, которое, мы надеемся, использует любопытно повторяющиеся шаблоны законным образом и требует только получения класса и предоставления подходящего конструктора:

template <class Child, class T>
class Wrapper
{
    T n;
public:
    Wrapper(T n = T()): n(n) {}
    T& value() { return n; }
    T value() const { return n; }
    Child operator+= (Wrapper other) { return Child(n += other.n); }
    //... many other operators
};

template <class Child, class T>
Child operator+(Wrapper<Child, T> lhv, Wrapper<Child, T> rhv)
{
    return Wrapper<Child, T>(lhv) += rhv;
}

//Make two different kinds of "int"'s

struct IntA : public Wrapper<IntA, int>
{
    IntA(int n = 0): Wrapper<IntA, int>(n) {}
};

struct IntB : public Wrapper<IntB, int>
{
    IntB(int n = 0): Wrapper<IntB, int>(n) {}
};

#include <iostream>

int main()
{
    IntA a1 = 1, a2 = 2, a3;
    IntB b1 = 1, b2 = 2, b3;
    a3 = a1 + a2;
    b3 = b1 + b2;
    //a1 + b1;  //bingo
    //a1 = b1; //bingo
    a1 += a2;

    std::cout << a1.value() << ' ' << b3.value() << '\n';
}

Но если вы примете совет, что вы должны просто определить новый тип и перегрузить операторы, вы можете взглянуть на Boost.Operators

Ответ 11

Ну, вам действительно не нужно наследовать все, что не имеет никаких виртуальных функций-членов. Поэтому даже если int был классом, не было бы плюса над композицией.

Итак, виртуальное наследование - единственная реальная причина, по которой вам нужно наследование в любом случае; все остальное просто экономит массу времени набрав. И я не думаю, что класс/тип int с виртуальными членами будет самой умной идеей представить в мире С++. По крайней мере, не для вас каждый день int.

Ответ 12

Что значит наследовать от int?

"int" не имеет функций-членов; он не имеет данных-членов, это 32-битное представление в памяти. У него нет собственного vtable. Все, что у него есть "(на самом деле это даже не их собственные) - это некоторые операторы, такие как + -/*, которые являются более глобальными функциями, чем функции-члены.

Ответ 13

Вы можете получить то, что хотите, с сильными typedef. См. BOOST_STRONG_TYPEDEF

Ответ 14

Более общий, чем тот факт, что "int является примитивным": int - это скалярный тип, а классы - aggregate. Скаляр является атомным значением, а совокупность - чем-то с членами. Наследование (по крайней мере, поскольку оно существует в С++) имеет смысл только для совокупного типа, потому что вы не можете добавлять члены или методы в скаляры - по определению у них нет каких-либо членов.

Ответ 15

Этот ответ представляет собой реализацию ответа UncleBens

положить в Primitive.hpp

#pragma once

template<typename T, typename Child>
class Primitive {
protected:
    T value;

public:

    // we must type cast to child to so
    // a += 3 += 5 ... and etc.. work the same way
    // as on primitives
    Child &childRef(){
        return *((Child*)this);
    }

    // you can overload to give a default value if you want
    Primitive(){}
    explicit Primitive(T v):value(v){}

    T get(){
        return value;
    }

    #define OP(op) Child &operator op(Child const &v){\
        value op v.value; \
        return childRef(); \
    }

    // all with equals
    OP(+=)
    OP(-=)
    OP(*=)
    OP(/=)
    OP(<<=)
    OP(>>=)
    OP(|=)
    OP(^=)
    OP(&=)
    OP(%=)

    #undef OP

    #define OP(p) Child operator p(Child const &v){\
        Child other = childRef();\
        other p ## = v;\
        return other;\
    }

    OP(+)
    OP(-)
    OP(*)
    OP(/)
    OP(<<)
    OP(>>)
    OP(|)
    OP(^)
    OP(&)
    OP(%)

    #undef OP


    #define OP(p) bool operator p(Child const &v){\
        return value p v.value;\
    }

    OP(&&)
    OP(||)
    OP(<)
    OP(<=)
    OP(>)
    OP(>=)
    OP(==)
    OP(!=)

    #undef OP

    Child operator +(){return Child(value);}
    Child operator -(){return Child(-value);}
    Child &operator ++(){++value; return childRef();}
    Child operator ++(int){
        Child ret(value);
        ++value;
        return childRef();
    }
    Child operator --(int){
        Child ret(value);
        --value;
        return childRef();
    }

    bool operator!(){return !value;}
    Child operator~(){return Child(~value);}

};

Пример:

#include "Primitive.hpp"
#include <iostream>

using namespace std;
class Integer : public Primitive<int, Integer> {
public:
    Integer(){}
    Integer(int a):Primitive<int, Integer>(a) {}

};
int main(){
    Integer a(3);
    Integer b(8);

    a += b;
    cout << a.get() << "\n";
    Integer c;

    c = a + b;
    cout << c.get() << "\n";

    cout << (a > b) << "\n";
    cout << (!b) << " " << (!!b) << "\n";

}

Ответ 16

Пожалуйста, извините меня за моего бедного английского.

Существует большая разница между правильной конструкцией С++:

struct Length { double l; operator =!?:%+-*/...(); };
struct Mass { double l; operator =!?:%+-*/...(); };

и предлагаемое расширение

struct Length : public double ;
struct Mass   : public double ;

И это различие заключается в поведении ключевого слова this. this - это указатель и использование указателя позволяет мало шансов использовать регистры для вычислений, потому что в обычных процессорах регистры не имеют адреса. Хуже, используя указатель, сделать компилятор подозрительным в том, что два указателя могут обозначать одну и ту же память.

Это поставит чрезвычайную нагрузку на компилятор для оптимизации тривиальных операций.

Еще одна проблема заключается в количестве ошибок: воспроизведение точно всего поведения операторов является абсолютной ошибкой (для того, чтобы сделать конструктор явным, не запрещает все случаи implicits). Вероятность ошибки при построении такого объекта довольно высока. Это не эквивалентно тому, чтобы иметь возможность сделать что-то через тяжелую работу или сделать это уже сделано.

Разработчики компилятора будут вводить код проверки типов (возможно, некоторые ошибки, но точность компилятора намного лучше, чем клиентский код, из-за какой-либо ошибки в компиляторе генерируют бесчисленные ошибки), но основное поведение операции останется неизменным, как и несколько ошибок, чем обычно.

Предлагаемое альтернативное решение (с использованием структур во время фазы отладки и реальных поплавков при оптимизации) интересно, но имеет свои недостатки: оно повышает вероятность появления ошибок только в оптимизированной версии. Оптимизированное приложение для отладки является дорогостоящим.

Можно реализовать хорошее предложение для первоначального запроса @Rocketmagnet для целых типов, используя:

enum class MyIntA : long {}; 
auto operator=!?:%+-*/...(MyIntA);
MyIntA operator "" _A(long);

Уровень ошибки будет довольно высоким, например, с помощью трюка с одним членом, но компилятор будет обрабатывать типы thoses точно так же, как встроенные целые числа (включая возможность регистрации и оптимизацию), спасибо за inlining.

Но этот трюк нельзя использовать (к сожалению) для чисел с плавающей запятой, и самая приятная потребность - это, очевидно, проверка достоверных размеров. Нельзя смешивать яблоки и груши: добавление длины и площади является общей ошибкой.

Вызов Страустрапа by @Jerry не имеет значения. Виртуальность имеет смысл главным образом для общественного наследования, и потребность здесь в частном наследовании. Рассмотрение вокруг "хаотических" правил преобразования C (имеет ли С++ 14 что-то не хаотичное?) Базового типа также не полезно: цель состоит в том, чтобы не иметь правил преобразования по умолчанию, а не следовать стандартным.

Ответ 17

Если я помню, это была основная или одна из главных причин, по которой С++ не считался истинным объектно-ориентированным языком. Java-люди сказали бы: "В Java все равно является объектом";)

Ответ 18

Это связано с тем, как элементы хранятся в памяти. Int в С++ является интегральным типом, как упоминалось в другом месте, и это всего лишь 32 или 64 бита (слово) в памяти. Однако объект хранится по-разному в памяти. Он обычно хранится в куче, и он имеет функциональность, связанную с полиморфизмом.

Я не знаю, как объяснить это лучше. Как бы вы наследовали от числа 4?

Ответ 19

Почему вы не можете наследовать от int, даже если вы захотите?

Производительность

Нет никаких функциональных причин, по которым вы не должны уметь (на произвольном языке) наследовать от обычных типов, таких как int или char, или char * и т.д. Некоторые языки, такие как Java и Objective-C на самом деле предоставить класс/объект (в штучной упаковке) версии базового типа, чтобы удовлетворить эту потребность (а также справиться с некоторыми другими неприятными последствиями порядковых типов, не являющихся объектами):

language     ordinal type boxed type, 
c++          int          ?
java         int          Integer
objective-c  int          NSNumber

Но даже Java и Objective-C сохраняют свои порядковые типы для использования... почему?

Простыми причинами являются производительность и потребление памяти. Порядковый тип обычно может быть сконструирован, эксплуатирован и передан по значению только в одной или двух инструкциях X86 и потребляет всего лишь несколько байтов в худшем случае. Класс обычно не может - он часто использует 2x или более столько же памяти, и манипулирование его значением может занять много сотен циклов.

Это означает, что программисты, которые это понимают, обычно используют порядковые типы для реализации чувствительного к производительности или памяти кода и требуют, чтобы разработчики языка поддерживали базовые типы.

Следует отметить, что довольно много языков не имеют порядковых типов, в частности динамические языки, такие как perl, который почти полностью опирается на вариационный тип, что совсем другое, и разделяет некоторые накладные расходы классов.