为什么我不能在C ++中继承int?

时间:2010-01-26 22:07:18

标签: c++ inheritance integer language-design strong-typing

我希望能够做到这一点:

class myInt : public int
{

};

为什么我不能?

我为什么要这样?更强的打字。例如,我可以定义两个类intAintB,这样我就可以intA + intAintB + intB,而不是intA + intB

“Ints不是课程。”那是什么?

“Ints没有任何会员数据。”是的,他们有,他们有32位,或其他什么。

“Ints没有任何会员功能。”好吧,他们有很多运营商,如+-

19 个答案:

答案 0 :(得分:81)

Neil的评论非常准确。 Bjarne提到考虑并拒绝这种确切的可能性 1

  

过去的初始化语法   内置类型非法。允许   它,我介绍了这个概念   内置类型有构造函数和   析构函数。例如:

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

我考虑过将这个概念扩展到   允许从内置类派生   并明确声明内置   内置类型的运算符。然而,   我克制自己。

     

允许   来自int的推导没有   实际上给了一个C ++程序员   与...相比有什么新鲜事   拥有int成员。这是   主要是因为int没有   派生的任何虚函数   要覆盖的类更严重的是   但是,C转换规则是如此   混淆假装int,   short等表现良好   普通班不会奏效。   它们要么是C兼容的,要么是它们   服从相对良好的C ++   类的规则,但不是两者。

就评论而言,表现不能成为一个类,它(至少大部分)是假的。在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。

答案 1 :(得分:52)

Int是序数类型,而不是类。你为什么要这样做?

如果需要向“int”添加功能,请考虑构建具有整数字段的聚合类,以及公开所需的任何其他功能的方法。

<强>更新

@OP “Ints不是类”所以?

继承,多态和封装是面向对象设计的基石。这些都不适用于序数类型。你不能从int继承,因为它只是一堆字节而且没有代码。

Ints,chars和其他序数类型没有方法表,因此无法添加方法或覆盖它们,这实际上是继承的核心。

答案 2 :(得分:25)

  

我为什么要这样?更强的打字。例如,我可以定义两个类intA和intB,这让我可以执行intA + intA或intB + intB,但不能执行intA + intB。

这没有任何意义。你可以在不继承任何东西的情况下做到这一切。 (另一方面,我看不出你如何使用继承来实现它。)例如,

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

填写空白,你有一个解决问题的类型。您可以执行SpecialInt + SpecialIntint + int,但SpecialInt + int将无法完全按照您的需要进行编译。

另一方面,如果我们假装继承自int是合法的,而我们的SpecialInt派生自int,那么SpecialInt + int 编译。继承会导致您想要避免的确切问题。 继承可以轻松避免问题。

  

“Ints没有任何会员功能。”好吧,他们有很多运营商,比如+和 - 。

但这些不是成员函数。

答案 3 :(得分:14)

如果OP真的想了解为什么C ++就是这样,那么他应该掌握Stroustup的书“C ++的设计和演变”。它解释了在C ++早期这个以及许多其他设计决策的基本原理。

答案 4 :(得分:10)

因为int是本机类型而不是类

编辑:将我的评论移到我的答案中。

它来自C遗产以及原始人所代表的东西。 c ++中的原语只是一个字节集合,除了编译器之外几乎没有什么意义。另一方面,类有一个函数表,一旦你开始继承继承和虚拟继承路径,那么你就有了一个vtable。这些都不存在于原语中,并且通过使它存在,你会a)打破许多c代码,假设int只有8个字节,b)使程序占用更多的内存。

以另一种方式思考。 int / float / char没有任何数据成员或方法。把这些原语想象成夸克 - 它们是你无法细分的构建块,你用它们来制造更大的东西(道歉,如果我的类比稍微偏离,我不知道足够的粒子物理学)

答案 5 :(得分:9)

在c ++

中强整类型的int(和浮动等)

Scott MeyerEffective 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

然后,您将TimeMassDistance定义为所有(且仅限)适当运算符重载到相应操作的类。在伪代码中:

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) {}
}

完成此操作后,您的编译器会告诉您违反上述类中编码规则的任何地方。

我会让你自己解决剩下的细节,或者买这本书。

答案 6 :(得分:5)

其他人说的是真的... int是C ++中的原语(很像C#)。但是,只需围绕int

构建一个类,您就可以实现所需的目标
class MyInt
{
private:
   int mInt;

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

然后,您可以继承您所需的一切。

答案 7 :(得分:4)

没有人提到C ++被设计为(大部分)与C的向后兼容性,以便简化C编码器的升级路径,从而struct默认所有成员公共等。

int作为基类,你可以覆盖会从根本上使该规则复杂化并使编译器实现地狱般的,如果你希望现有的编码器和编译器供应商支持你的羽翼未丰的语言可能不值得努力

答案 8 :(得分:3)

正如我所说的那样,因为int是原始类型所以无法完成。

但是,我理解动机,如果是为了更强的打字。甚至为C ++ 0x建议special kind of 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

答案 9 :(得分:3)

在C ++中,内置类型不是类。

答案 10 :(得分:2)

好吧,你真的不需要继承没有任何虚拟成员函数的东西。因此,即使int 是一个类,也不会超过组合。

所以说,虚拟继承是你无论如何都需要继承的唯一真正原因;其他一切只是为你节省大量的打字时间。而且我认为具有虚拟成员的int类/类型不是C ++世界中最聪明的想法。至少每天都不适合你int

答案 11 :(得分:1)

从int继承是什么意思?

“int”没有成员函数;它没有成员数据,它是内存中的32(或64)位表示。它没有自己的vtable。所有它“拥有”(它甚至不拥有它们)都是像+ - / *这样的运算符,它们实际上是比成员函数更全局的函数。

答案 12 :(得分:1)

你可以通过强大的typedef得到你想要的东西。见BOOST_STRONG_TYPEDEF

答案 13 :(得分:0)

比“int is primitive”这样的事实更为普遍:int标量类型,而类是聚合类型。标量是原子值,而聚合是成员的东西。继承(至少在C ++中存在)只对聚合类型有意义,因为您无法向标量添加成员或方法 - 根据定义,它们没有任何成员。

答案 14 :(得分:0)

这个答案是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";

}

答案 15 :(得分:0)

请原谅我英语不好。

C ++正确构造之间存在一个主要区别:

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

和建议的扩展名

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

这种差异取决于关键字this的行为。 this是一个指针,使用指针几乎没有机会使用寄存器进行计算,因为在通常情况下,处理器寄存器没有地址。最糟糕的是,使用指针使编译器怀疑两个指针可能指定相同的内存这一事实。

这将给编译器带来非常大的负担,以优化trivial ops。

另一个问题是错误的数量:完全复制运算符的所有行为绝对容易出错(因为前者使构造函数显式不禁止所有的implicits情况)。构建这样的对象时出错的可能性非常高。它并不等同于通过努力工作或已经完成工作的可能性。

编译器实现者会引入类型检查代码(可能有一些错误,但编译器的准确性比客户端代码好得多,因为编译器中的任何错误都会产生无数错误的情况),但操作的主要行为将保持完全相同,错误比平常少。

建议的备用解决方案(在调试阶段使用结构体,在优化阶段使用实际浮动体)很有意思但有缺点:它提高了仅在优化版本中出现错误的可能性。调试优化的应用程序成本很高。

可以使用以下方法为@Rocketmagnet对整数类型的初始需求实施一个好的提议:

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

错误级别会非常高,就像使用单个成员技巧一样,但编译器会像内置整数一样处理这些类型(包括寄存器功能和优化),这要归功于内联。

但是这个技巧不能(遗憾地)用于浮动数字,而最好的需求显然是真正有价值的维度检查。人们可能不会混淆苹果和梨:添加长度和面积是一个常见错误。

Stroustrup的&#39; @Jerry的调用无关紧要。虚拟性主要用于公共继承,并且需要私有继承。围绕“混乱”的思考基本类型的C转换规则(C ++ 14有什么不混乱吗?)也没用:目标是没有默认的转换规则,不遵循标准转换规则。

答案 16 :(得分:-1)

如果我记得,这是C ++不被认为是真正的面向对象语言的主要原因或其中一个主要原因。 Java人会说:“在Java中,一切都是对象”;)

答案 17 :(得分:-3)

为什么你不能继承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指令中构造,操作和传递值,并且在最坏的情况下仅消耗几个字节。一个类通常不能 - 它通常使用2倍或更多的内存,并且操纵它的值可能需要数百个周期。

这意味着理解这一点的程序员通常会使用序数类型来实现性能或内存使用敏感代码,并要求语言开发人员支持基类型。

应该注意的是,很多语言没有序数类型,特别是动态语言,例如perl,它几​​乎完全依赖于可变类型,这完全是另外一种类型,并且共享一些课程的开销。

答案 18 :(得分:-3)

这与项目如何存储在内存中有关。如其他地方所述,C ++中的int是一个整数类型,在内存中只是32或64位(一个字)。但是,对象在存储器中的存储方式不同。它通常存储在堆上,并且具有与多态性相关的功能。

我不知道如何更好地解释它。你将如何从4号继承?