75

我很想能够做到这一点:

class myInt : public int
{

};

为什么我不能?

为什么我想要?更强的打字。例如,我可以定义两个类intAand intB,这让我可以做intA + intAor intB + intB,但不能intA + intB

“整数不是类。” 所以呢?

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

“整数没有任何成员函数。” 好吧,他们有一大堆运算符,比如+and -

4

19 回答 19

88

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

初始化器语法过去对于内置类型是非法的。为此,我引入了内置类型具有构造函数和析构函数的概念。例如:

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

我考虑扩展这个概念以允许从内置类派生和显式声明内置类型的内置运算符。不过,我克制住了自己。

与拥有一个成员相比,允许从 an 派生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 节。

于 2010-01-27T00:22:56.100 回答
52

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

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

更新

@OP “整数不是类”是吗?

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

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

于 2010-01-26T22:10:35.623 回答
24

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

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

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

填空,你就有了一个可以解决你的问题的类型。您可以执行SpecialInt + SpecialIntor int + int,但SpecialInt + int不会完全按照您的意愿进行编译。

另一方面,如果我们假装从 int 继承是合法的,并且我们的SpecialInt派生自int,那么SpecialInt + int 就会编译。继承会导致您想要避免的确切问题。继承很容易避免这个问题。

“整数没有任何成员函数。” 好吧,他们有一大堆像 + 和 - 这样的运算符。

这些不是成员函数。

于 2010-01-26T22:48:40.077 回答
13

c++ 中整数(和浮点数等)的强类型

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

然后,您将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) {}
}

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

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

于 2010-01-27T00:43:49.093 回答
10

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

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

它来自 C 遗产,以及原语所代表的确切含义。c++ 中的原语只是一个字节的集合,除了编译器之外没有什么意义。另一方面,一个类有一个函数表,一旦你开始沿着继承和虚拟继承路径走下去,你就有了一个 vtable。这些都不存在于原语中,通过使其存在,您将 a) 破坏许多假定 int 仅为 8 个字节的 c 代码,并且 b) 使程序占用更多内存。

换个方式想一想。int/float/char 没有任何数据成员或方法。把基元想象成夸克——它们是你无法细分的构建块,你用它们来制造更大的东西(如果我的类比有点不对,我很抱歉,我对粒子物理学了解的不够多)

于 2010-01-26T22:08:54.363 回答
5

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

拥有int一个可以覆盖的基类将从根本上使该规则复杂化,并使编译器实现变得地狱般,如果您希望现有的编码器和编译器供应商支持您刚刚起步的语言,这可能不值得付出努力。

于 2010-01-27T00:35:07.720 回答
4

其他人所说的是真的……int是 C++ 中的原语(很像 C#)。但是,您可以通过构建一个类来实现您想要的int

class MyInt
{
private:
   int mInt;

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

然后你可以继承你想要的一切。

于 2010-01-26T22:24:15.400 回答
4

正如我所说的其他人,由于 int 是原始类型,因此无法完成。

不过,我理解动机,如果它是为了更强的打字。甚至有人为 C++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

于 2010-01-26T23:22:22.520 回答
3

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

于 2010-01-26T22:08:41.370 回答
2

好吧,你真的不需要继承任何没有任何虚拟成员函数的东西。因此,即使int 一堂课,也不会有超过作文的加分项。

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

于 2010-01-27T00:36:40.937 回答
2

你可以通过强类型定义得到你想要的。见BOOST_STRONG_TYPEDEF

于 2010-05-02T13:59:51.463 回答
2

这是一个非常古老的话题,但仍然与许多人相关。

单元感知编程提供了一个非常重要的原因,为什么从内在/基本类型继承在 C++ 中很有价值。现在有许多成熟的解决方案来解决这个问题,但所有这些解决方案都需要模板才能实现原本可以通过继承、多态性和 C++ 的强类型检查直接处理的功能。以下是单元感知编程的一种替代方案:

https://benjaminjurke.com/content/articles/2015/compile-time-numerical-unit-dimension-checking/

从原始指针继承也是有意义的(也是非法的)。我们可以轻松地创建一个从智能指针继承的类,从而扩展其行为,但由于 C++ 的限制,我们无法对原始指针执行相同的操作。这意味着我们扩展原始指针(如 char*)行为的唯一方法是编写需要将指针作为参数传递给函数的函数,而不是让它们看起来更像您在 std::string 中看到的方法。

当然,我们总是可以使用组合而不是继承来获得相同的效果,但是这样做,我们会丢失所有的内在操作(例如对于 char* 的 operator[] 和 operator++)并且必须重新映射它们中的每一个我们需要支持。可行吗?当然。我已经做到了。这简单吗?不必要。这取决于您需要映射多少以及需要多快才能完成。快吗?要看。

综上所述,在我看来,支持从内在类型(包括原始指针类型)继承行为的最大论据是它证明了语言的概念一致性。总体而言,C++ 是非常一致的,但在我的书中它有点崩溃,因为内部类型被认为是特殊的,坦率地说,它们不是。

于 2021-10-15T02:33:48.267 回答
1

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

}
于 2014-10-02T03:07:01.270 回答
1

从 int 继承是什么意思?

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

于 2010-01-26T22:13:11.110 回答
0

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

于 2010-01-26T23:40:34.357 回答
0

请原谅我糟糕的英语。

像这样的 C++ 正确构造之间有一个主要区别:

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

和提议的延期

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

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

这将给编译器带来巨大的负担,以优化琐碎的操作。

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

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

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

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

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

错误级别将非常高,例如使用单个成员技巧,但编译器会将这些类型完全视为内置整数(包括寄存器功能和优化),感谢内联。

但是这个技巧不能(遗憾地)用于浮点数,最好的需求显然是实值尺寸检查。一个人可能不会混淆苹果和梨:增加长度和面积是一个常见的错误。

@Jerry 对 Stroustrup 的调用是无关紧要的。虚拟性主要对公共继承有意义,而这里需要的是私有继承。考虑基本类型的“混乱”C 转换规则(C++14 有什么不混乱的吗?)也没有用:目标是没有默认转换规则,而不是遵循标准转换规则。

于 2015-08-18T14:24:48.287 回答
-1

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

于 2010-01-26T22:27:04.400 回答
-3

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

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

于 2010-01-26T22:56:33.943 回答
-4

为什么你不能从 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,它几乎完全依赖于可变参数类型,这完全是另外一回事,并且共享一些类的开销。

于 2010-01-27T00:01:40.750 回答