web-dev-qa-db-fra.com

Pourquoi ne puis-je pas hériter de int en C++?

J'aimerais pouvoir faire ça:

class myInt : public int
{

};

Pourquoi je ne peux pas?

Pourquoi voudrais-je? Taper plus fort. Par exemple, je pourrais définir deux classes intA et intB, ce qui me permet de faire intA + intA ou intB + intB, mais pas intA + intB.

"Ints ne sont pas des classes." Et alors?

"Ints ne possède aucune donnée de membre." Oui, ils ont 32 bits, ou autre chose.

"Ints n’a aucune fonction membre." Eh bien, ils ont tout un tas d’opérateurs comme + et -.

54
Rocketmagnet

Le commentaire de Neil est assez précis. Bjarne a mentionné envisager et rejeter cette possibilité exacte1:

Auparavant, la syntaxe de l'initialiseur était Illégale pour les types intégrés. Pour permettre , J’ai introduit la notion selon laquelle les types intégrés Ont des constructeurs et des destructeurs . Par exemple:

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

J'ai envisagé d'étendre cette notion à autoriser la dérivation à partir des classes intégrées Et la déclaration explicite d'opérateurs intégrés Pour les types intégrés. Cependant, Je me suis retenu.

Autoriser la dérivation de À partir de intne donne pas À un programmeur C++ Réellement nouveau par rapport à Ayant un membre intname__. Ceci est principalement lié au fait que intne possède Aucune fonction virtuelle à remplacer par la classe dérivée . Plus sérieusement Cependant, les règles de conversion C sont tellement chaotiques que prétendre que intname __, shortname__, etc., sont bien comportés Les classes ordinaires ne vont pas pour travailler. Ils sont soit compatibles C, soit obéissent aux règles relativement sage de C++ pour les classes, mais pas les deux.

En ce qui concerne le commentaire que la performance justifie de ne pas faire d'une classe int, c'est (du moins pour la plupart) faux. Dans Smalltalk, tous les types sont des classes - mais presque toutes les implémentations de Smalltalk ont ​​des optimisations, de sorte que l'implémentation peut être essentiellement identique à la façon dont vous feriez fonctionner un type sans classe. Par exemple, la classe smallInteger représente un entier de 15 bits et le message '+' est codé en dur dans la machine virtuelle. Ainsi, même si vous pouvez dériver de smallInteger, les performances sont similaires à celles d'un type intégré ( bien que Smalltalk soit suffisamment différent de C++, les comparaisons directes des performances sont difficiles et peu susceptibles d’être significatives).

Edit: le bit "gaspillé" dans l'implémentation SmallTalk de smallInteger ne serait probablement pas nécessaire en C ou C++. Smalltalk est un peu comme Java - lorsque vous "définissez un objet", vous définissez en fait un pointeur sur un objet, et vous devez allouer dynamiquement un objet sur lequel il doit pointer. Ce que vous manipulez, passez à une fonction en tant que paramètre, etc., est toujours juste le pointeur, pas l'objet lui-même.

Ce n'est pas pas comment smallInteger est implémenté - dans ce cas, ils placent la valeur entière directement dans ce qui serait normalement le pointeur. Pour faire la distinction entre un petit entier et un pointeur, ils obligent tous les objets à être alloués à des limites d'octets égales, ainsi le LSB est toujours clair. Un smallInteger a toujours le LSB défini.

La plupart de cela est toutefois nécessaire, car Smalltalk est typé de manière dynamique - il doit être capable de déduire le type en regardant la valeur elle-même, et smallInteger utilise ce LSB comme balise de type. Étant donné que C++ est typé de manière statique, il n'est jamais nécessaire de déduire le type de la valeur, vous n'avez donc probablement pas besoin de gaspiller ce bit.

1 Dans Conception et évolution de C++ , §15.11.3.

80
Jerry Coffin

Int est un type ordinal, pas une classe. Pourquoi voudriez-vous?

Si vous devez ajouter des fonctionnalités à "int", envisagez de créer une classe agrégée comportant un champ entier et des méthodes exposant les fonctionnalités supplémentaires dont vous avez besoin.

Mise à jour

@OP "Les classes ne sont pas des classes" alors?  

Héritage , polymorphisme et encapsulation sont les clés de voûte de conception orientée objet . Aucune de ces choses ne s'applique aux types ordinaux. Vous ne pouvez pas hériter d'un int parce que c'est juste un tas d'octets et qu'il n'a pas de code. 

Ints, chars et autres types ordinaux n'ont pas tables de méthodes , il n'y a donc aucun moyen d'ajouter des méthodes ou de les remplacer, ce qui est vraiment le cœur de l'héritage.

53
3Dave

Pourquoi voudrais-je? Taper plus fort. Par exemple, je pourrais définir deux classes intA et intB, ce qui me permet de faire intA + intA ou intB + intB, mais pas intA + intB.

Ça n'a aucun sens. Vous pouvez faire tout cela sans rien hériter. (Et d'autre part, je ne vois pas comment vous pourriez y arriver en utilisant l'héritage.) Par exemple,

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

Remplissez les blancs et vous avez un type qui résout votre problème. Vous pouvez faire SpecialInt + SpecialInt ou int + int, mais SpecialInt + int ne compilera pas, exactement comme vous le souhaitiez.

D'un autre côté, si nous prétendions qu'hériter de int était légal et que notre SpecialInt dérive de int, alors SpecialInt + int serait compile. L'héritage causerait le problème exact que vous voulez éviter. Not hériter évite le problème facilement.

"Ints n’a aucune fonction membre." Eh bien, ils ont tout un tas d’opérateurs comme + et -.

Ce ne sont pas des fonctions membres cependant. 

26
jalf

Si le PO veut vraiment comprendre pourquoi C++ est ce qu'il est, il devrait alors se procurer un exemplaire du livre de Stroustup "La conception et l'évolution du C++". Il explique la raison de cela et de nombreuses autres décisions de conception dans les débuts de C++.

14
Stephen C. Steel

Parce que int est un type natif et non une classe

Edit: déplacer mes commentaires dans ma réponse.

Cela vient de l'héritage C et de ce que représentent exactement les primitifs. Une primitive en c ++ est simplement une collection d’octets qui ont peu de signification sauf pour le compilateur. Une classe, par contre, a une table de fonctions et une fois que vous commencez à descendre le chemin de l'héritage et du chemin d'héritage virtuel, vous avez une table vtable. Rien de tout cela n'est présent dans une primitive, et en la rendant présente, vous pourriez a) casser beaucoup de code c qui suppose qu'un int est de 8 octets seulement et b) obliger les programmes à utiliser beaucoup plus de mémoire.

Pensez-y d'une autre manière. int/float/char n'ont aucun membre de données ou méthode. Pensez aux primitives en tant que quarks - ce sont les blocs de construction que vous ne pouvez pas subdiviser, vous les utilisez pour créer de plus grandes choses (excuses si mon analogie est un peu fausse, je ne connais pas assez de physique des particules)

10
Mason

typage fort des ints (et des floats, etc.) en c ++

Scott Meyer ( Effective c ++ a une solution très efficace et puissante à votre problème de typage fort des types de base en c ++, et cela fonctionne comme suit:

Le typage fort est un problème qui peut être traité et évalué au moment de la compilation , ce qui signifie que vous pouvez utiliser les ordinaux (typage faible) pour plusieurs types au moment de l'exécution dans les applications déployées, et utiliser une phase de compilation spéciale pour: Éliminez les combinaisons inappropriées de types au moment de la compilation.

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

Vous définissez ensuite votre Time, Mass, Distance comme étant des classes avec tous (et seulement) les opérateurs appropriés surchargés aux opérations appropriées. En pseudo-code:

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

Une fois cela fait, votre compilateur vous indiquera les endroits où vous avez violé les règles encodées dans les classes ci-dessus.

Je vous laisserai régler vous-même le reste des détails ou acheter le livre.

9
Alex Brown

Ce que d'autres ont dit est vrai ... int est une primitive en C++ (un peu comme C #). Cependant, vous pouvez réaliser ce que vous vouliez en construisant simplement une classe autour de int:

class MyInt
{
private:
   int mInt;

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

Vous pouvez alors en hériter tout ce que vous voulez.

5
Polaris878

Personne n'a mentionné que C++ avait été conçu pour avoir (principalement) une compatibilité ascendante avec C, afin de faciliter le chemin de mise à niveau pour les codeurs C, donc struct par défaut à tous les membres public, etc.

Avoir int comme classe de base que vous pouvez remplacer vous compliquerait fondamentalement cette règle et rendrait l’implémentation du compilateur difforme, si vous voulez que les codeurs et les fournisseurs de compilateurs existants prennent en charge votre langage naissant ne valait probablement pas la peine.

4
zebrabox

En C++, les types intégrés ne sont pas des classes.

3
Trent

Comme d’autres, je ne peux pas le faire car int est un type primitif.

Je comprends la motivation, cependant, s’il s’agit de taper plus fort. Il a même été proposé pour C++ 0x qu'un type spécial de typedef soit suffisant pour cela (mais cela a été rejeté?).

Peut-être que quelque chose pourrait être réalisé si vous fournissiez vous-même l'emballage de base. E.g quelque chose comme ce qui suit, qui utilise, espérons-le, des modèles curieusement récurrents d'une manière légale, et nécessite uniquement de dériver une classe et de fournir un constructeur approprié:

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';
}

Mais si vous conseillez de définir un nouveau type et de surcharger les opérateurs, vous pouvez consulter Boost.Operators

3
UncleBens

Eh bien, vous n'avez pas vraiment besoin d'hériter de tout ce qui n'a aucune fonction de membre virtuel. Donc, même si int était une classe, il n’y aurait pas de plus que la composition.

Ainsi, l’héritage virtuel est la seule raison réelle pour laquelle vous auriez besoin d’un héritage de toute façon; tout le reste ne fait que vous épargner beaucoup de temps de frappe. Et je ne pense pas qu'un classe/type int avec des membres virtuels serait la chose la plus intelligente à imaginer dans le monde C++. Du moins pas pour vous tous les jours int.

2
Debilski

Que signifie hériter d'un int?

"int" n'a pas de fonctions membres; il n'a pas de données de membre, c'est une représentation de 32 (ou 64) bits en mémoire. Il n'a pas sa propre table. Tout ce qu'il "a" (il ne les possède même pas vraiment) sont des opérateurs comme + -/* qui sont vraiment des fonctions plus globales que des fonctions membres.

1
anon

Vous pouvez obtenir ce que vous voulez avec des typedefs puissants. Voir BOOST_STRONG_TYPEDEF

Plus général que le fait que "int est primitive" est ceci: int est un scalar type, alors que les classes sont aggreg types. Un scalaire est une valeur atomique, alors qu'un agrégat est quelque chose avec des membres. L'héritage (du moins tel qu'il existe en C++) n'a de sens que pour un type d'agrégat, car vous ne pouvez pas ajouter de membres ni de méthodes aux scalaires. Par définition, ils ne possèdent aucun membre.

0
Chuck

S'il vous plaît, excusez-moi pour mon pauvre anglais.

Il y a une différence majeure entre la construction correcte C++ comme ceci:

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

et l'extension proposée 

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

Et cette différence repose sur le comportement du mot clé this. this est un pointeur et l'utilisation d'un pointeur laisse peu de chances d'utiliser des registres pour les calculs, car, dans la plupart des cas, les registres des processeurs n'ont pas d'adresse. Pire, utiliser un pointeur rend le compilateur méfiant sur le fait que deux pointeurs peuvent désigner la même mémoire.

Cela imposera au compilateur un fardeau extraordinaire pour optimiser les opérations triviales. 

Un autre problème concerne le nombre de bogues: reproduire exactement tout le comportement des opérateurs est totalement sujet aux erreurs (par exemple, rendre le constructeur explicite n'interdit pas tous les cas implicites). La probabilité d'erreur lors de la construction d'un tel objet est assez élevée. Ce n’est pas équivalent à avoir la possibilité de faire quelque chose par le travail dur ou de le faire déjà. 

Les implémenteurs du compilateur introduiraient du code de vérification de type (avec peut-être quelques erreurs, mais l'exactitude du compilateur est bien meilleure que le code client, à cause d'un bogue dans le compilateur générant d'innombrables erreurs), mais le comportement principal de l'opération restera exactement le même, avec peu d'erreurs que d'habitude.

La solution de rechange proposée (utiliser des structures lors de la phase de débogage et des valeurs réelles lorsque optimisées) est intéressante mais présente des inconvénients: elle augmente la probabilité d’avoir des bogues uniquement dans la version optimisée. Et le débogage d’une application optimisée est très coûteux.

On peut implémenter une bonne proposition pour la demande initiale @Rocketmagnet pour les types entiers en utilisant:

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

Le niveau de bogue sera assez élevé, comme si vous utilisiez une astuce à membre unique, mais le compilateur traitera ces types exactement comme les entiers intégrés (y compris la capacité de registre et l'optimisation), grâce à inlining.

Mais cette astuce ne peut pas être utilisée (malheureusement) pour les nombres flottants, et le plus besoin est évidemment de vérifier les dimensions réelles. On ne peut pas mélanger des pommes et des poires: l’ajout de la longueur et de la superficie est une erreur courante.

L'invocation de Stroustrup 'par @Jerry n'est pas pertinente. La virtualité a un sens principalement pour l'héritage public, et le besoin est là pour l'héritage privé. Les considérations relatives aux règles de conversion C "chaotiques" (le C++ 14 a-t-il quelque chose de non chaotique?) De type de base ne sont également pas utiles: l'objectif est de ne pas avoir de règles de conversion par défaut, de ne pas suivre les règles standard.

0
alta

Cette réponse est une implémentation de la réponse UncleBens

mettre en 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);}

};

Exemple:

#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";

}
0
over_optimistic