Comment interdire les temporaires

107

Pour une classe Foo, y a-t-il un moyen d'interdire sa construction sans lui donner un nom?

Par exemple:

Foo("hi");

Et ne l'autorisez que si vous lui donnez un nom, comme le suivant?

Foo my_foo("hi");

La durée de vie du premier n'est que la déclaration, et le second est le bloc englobant. Dans mon cas d'utilisation, Foomesure le temps entre le constructeur et le destructeur. Comme je ne fais jamais référence à la variable locale, j'oublie souvent de la mettre et je change accidentellement la durée de vie. Je voudrais plutôt obtenir une erreur de compilation.

Martin C. Martin
la source
8
Cela pourrait également être utile pour les gardes de verrouillage mutex.
lucas clemente
1
Eh bien, vous pourriez écrire votre propre compilateur C ++ là où il était interdit, mais à proprement parler ce ne serait pas C ++ alors. Il y a aussi des endroits où des temporaires comme ça seraient utiles, comme lors du retour d'un objet à partir d'une fonction par exemple (comme return std::string("Foo");)
Un programmeur mec
2
Non, vous ne pouvez pas faire ça, désolé
Armen Tsirunyan
2
Selon votre religion, cela pourrait être un cas où les macros pourraient être utiles (en utilisant ce type uniquement via une macro qui crée toujours une variable)
PlasmaHH
3
Cela ressemble plus à quelque chose que je voudrais que mon outil LINT attrape plutôt qu'à quelque chose que je voudrais empêcher syntaxiquement par un piratage du compilateur.
Warren P

Réponses:

101

Une autre solution macro-basée:

#define Foo class Foo

La déclaration Foo("hi");s'étend à class Foo("hi");, qui est mal formée; mais se Foo a("hi")développe class Foo a("hi"), ce qui est correct.

Cela présente l'avantage d'être à la fois compatible avec le code source et le code binaire existant (correct). (Cette affirmation n'est pas tout à fait correcte - veuillez consulter le commentaire de Johannes Schaub et la discussion qui s'ensuit ci-dessous: "Comment pouvez-vous savoir que la source est compatible avec le code existant? Son ami inclut son en-tête et a void f () {int Foo = 0;} qui précédemment compilé correctement et maintenant mal compilé! De plus, chaque ligne qui définit une fonction membre de la classe Foo échoue: void class Foo :: bar () {} " )

ecatmur
la source
51
Comment pouvez-vous savoir qu'il est compatible avec le code existant? Son ami inclut son en-tête et a void f() { int Foo = 0; }déjà compilé très bien et maintenant mal compilé! En outre, chaque ligne qui définit une fonction de membre de la classe Foo échoue: void class Foo::bar() {}.
Johannes Schaub - litb
21
Comment cela peut-il obtenir autant de votes? Il suffit de regarder le commentaire de @ JohannesSchaub-litb et vous comprendrez que c'est une très mauvaise solution. Parce que toutes les définitions des fonctions membres ne sont plus valables après cela .. -1 de mon côté
Aamir
2
@JustMaximumPower: J'espère que c'était sarcastique car sinon, c'est à nouveau une mauvaise solution de contournement (lire pire). Parce que nous sommes de retour à la case départ après l'avoir défini, ce qui signifie que vous n'obtiendrez pas d'erreur de compilation (ce que l'OP avait prévu) sur une ligne similaire, c'est- Foo("Hi")à- dire à l' intérieur de Foo.cpp maintenant
Aamir
1
@Aamir Non, je suis sérieux. Martin C. Martin a l'intention de l'utiliser pour protéger l'utilisation de Foo et non l'implémentation.
JustMaximumPower
1
J'ai essayé dans Visual Studio 2012 et j'ai trouvé que class Foo("hi");c'était OK pour la compilation.
fresky
71

Que diriez-vous d'un petit hack

class Foo
{
    public:
        Foo (const char*) {}
};

void Foo (float);


int main ()
{
    Foo ("hello"); // error
    class Foo a("hi"); // OK
    return 1;
}

la source
1
Super Hack! Une note: Foo a("hi");(sans class) serait également une erreur.
bitmask
Je ne suis pas sûr de comprendre. Foo ("bonjour") essaie d'appeler void Foo (float) et cela entraîne une erreur de l'éditeur de liens? Mais pourquoi la version float est-elle appelée au lieu du cteur Foo?
undu
2
undu, hm quel compilateur utilisez-vous? gcc 3.4 se plaint qu'il n'y a pas de conversion en float. Il tente d'appeler une fonction Foocar elle a priorité sur une classe.
@aleguna en fait, je n'ai pas essayé d'exécuter ce code, c'était juste une (mauvaise) supposition: s Mais tu as quand même répondu à ma question, je ne savais pas que la fonction avait priorité sur la classe.
undu
1
@didierc no, Foo::Foo("hi")n'est pas autorisé en C ++.
Johannes Schaub - litb
44

Rendez le constructeur privé mais donnez à la classe une méthode de création .

Dchhetri
la source
9
-1: Comment cela résout-il le problème du PO? Vous pouvez toujours écrire Foo::create();surFoo const & x = Foo::create();
Thomas Eding
@ThomasEding Je suppose que vous avez raison, cela ne résout pas le problème central d'OP, mais le force simplement à réfléchir et à ne pas faire l'erreur qu'il fait.
dchhetri
1
@ThomasEding vous ne pouvez pas vous protéger contre les utilisateurs en colère qui veulent briser le système. Même avec le hack de @ ecatmur, vous pouvez dire std::common_type<Foo>::type()et vous obtenez un temporaire. Ou même typedef Foo bar; bar().
Johannes Schaub - litb
@ JohannesSchaub-litb: Mais la grande différence est que ce soit par erreur ou non. Il n'y a presque aucun moyen de taper std::common_type<Foo>::type()par erreur. Laisser de côté le Foo const & x = ...par accident est totalement crédible.
Thomas Eding
24

Celui-ci n'entraîne pas une erreur du compilateur, mais une erreur d'exécution. Au lieu de mesurer un mauvais moment, vous obtenez une exception qui peut également être acceptable.

Tout constructeur que vous souhaitez protéger a besoin d'un argument par défaut sur lequel set(guard)est appelé.

struct Guard {
  Guard()
    :guardflagp()
  { }

  ~Guard() {
    assert(guardflagp && "Forgot to call guard?");
    *guardflagp = 0;
  }

  void *set(Guard const *&guardflag) {
    if(guardflagp) {
      *guardflagp = 0;
    }

    guardflagp = &guardflag;
    *guardflagp = this;
  }

private:
  Guard const **guardflagp;
};

class Foo {
public:
  Foo(const char *arg1, Guard &&g = Guard()) 
    :guard()
  { g.set(guard); }

  ~Foo() {
    assert(!guard && "A Foo object cannot be temporary!");
  }

private:
  mutable Guard const *guard;
}; 

Les caractéristiques sont:

Foo f() {
  // OK (no temporary)
  Foo f1("hello");

  // may throw (may introduce a temporary on behalf of the compiler)
  Foo f2 = "hello";

  // may throw (introduces a temporary that may be optimized away
  Foo f3 = Foo("hello");

  // OK (no temporary)
  Foo f4{"hello"};

  // OK (no temporary)
  Foo f = { "hello" };

  // always throws
  Foo("hello");

  // OK (normal copy)
  return f;

  // may throw (may introduce a temporary on behalf of the compiler)
  return "hello";

  // OK (initialized temporary lives longer than its initializers)
  return { "hello" };
}

int main() {
  // OK (it's f that created the temporary in its body)
  f();

  // OK (normal copy)
  Foo g1(f());

  // OK (normal copy)
  Foo g2 = f();
}

Le cas de f2, f3et le retour de "hello"ne pas être voulu. Pour éviter le rejet, vous pouvez autoriser la source d'une copie à être temporaire, en réinitialisant le guardpour nous garder maintenant au lieu de la source de la copie. Maintenant, vous voyez également pourquoi nous avons utilisé les pointeurs ci-dessus - cela nous permet d'être flexibles.

class Foo {
public:
  Foo(const char *arg1, Guard &&g = Guard()) 
    :guard()
  { g.set(guard); }

  Foo(Foo &&other)
    :guard(other.guard)
  {
    if(guard) {
      guard->set(guard);
    }
  }

  Foo(const Foo& other)
    :guard(other.guard)
  {
    if(guard) {
      guard->set(guard);
    }
  }

  ~Foo() {
    assert(!guard && "A Foo object cannot be temporary!");
  }

private:
  mutable Guard const *guard;
}; 

Les caractéristiques pour f2, f3et return "hello"sont maintenant toujours // OK.

Johannes Schaub - litb
la source
2
Foo f = "hello"; // may throwCela suffit pour me faire peur de ne jamais utiliser ce code.
Thomas Eding
4
@thomas, je recommande de marquer le constructeur explicit, puis ce code ne se compile plus. le but était de fotbid le temporaire, et c'est le cas. si vous avez peur, vous pouvez faire en sorte qu'il ne soit pas lancé en définissant la source d'une copie dans le constructeur de copie ou de déplacement sur une valeur non-temporaire. alors seul l'objet final de plusieurs copies peut être jeté s'il finit toujours par devenir temporaire.
Johannes Schaub - litb
2
Mon Dieu. Je ne suis pas novice en C ++ et C ++ 11, mais je ne comprends pas comment cela fonctionne. Pourriez-vous s'il vous plaît ajouter quelques explications? ..
Mikhail
6
@Mikhail l'ordre de destruction des objets temporaires qui sont détruits aux mêmes points est l'ordre inverse de leur construction. L'argument par défaut transmis par l'appelant est temporaire. Si l' Fooobjet est également temporaire et que sa durée de vie se termine par la même expression que l'argument par défaut, alors le Foodtor de l' objet sera appelé avant le dtor de l'argument par défaut, car le premier a été créé après le second.
Johannes Schaub - litb
1
@ JohannesSchaub-litb Très belle astuce. J'ai vraiment pensé qu'il est impossible de distinguer Foo(...);et Foo foo(...);de l'intérieur du Foo.
Mikhail
18

Il y a quelques années, j'ai écrit un correctif pour le compilateur GNU C ++ qui ajoute une nouvelle option d'avertissement pour cette situation. Ceci est suivi dans un élément Bugzilla .

Malheureusement, GCC Bugzilla est un cimetière où les suggestions de fonctionnalités bien pensées incluses dans les correctifs vont mourir. :)

Cela a été motivé par le désir d'attraper exactement le genre de bogues qui font l'objet de cette question dans un code qui utilise des objets locaux comme gadgets pour verrouiller et déverrouiller, mesurer le temps d'exécution, etc.

Kaz
la source
9

En l'état, avec votre implémentation, vous ne pouvez pas faire cela, mais vous pouvez utiliser cette règle à votre avantage:

Les objets temporaires ne peuvent pas être liés à des références non const

Vous pouvez déplacer le code de la classe vers une fonction autonome qui prend un paramètre de référence non const. Si vous le faites, vous obtiendrez une erreur du compilateur si un temporaire tente de se lier à la référence non const.

Exemple de code

class Foo
{
    public:
        Foo(const char* ){}
        friend void InitMethod(Foo& obj);
};

void InitMethod(Foo& obj){}

int main()
{
    Foo myVar("InitMe");
    InitMethod(myVar);    //Works

    InitMethod("InitMe"); //Does not work  
    return 0;
}

Production

prog.cpp: In function int main()’:
prog.cpp:13: error: invalid initialization of non-const reference of type Foo&’ from a temporary of type const char*’
prog.cpp:7: error: in passing argument 1 of void InitMethod(Foo&)’
Alok Save
la source
1
@didierc: à condition qu'ils fournissent une fonction supplémentaire. C'est à vous de ne pas le faire. Nous essayons de modifier un moyen de réaliser quelque chose qui n'est pas explicitement autorisé par la norme, donc bien sûr il y aura des restrictions.
Alok Enregistrer
@didierc le paramètre xest un objet nommé, il n'est donc pas clair si nous voulons vraiment l'interdire. Si le constructeur que vous auriez utilisé est explicite, les gens le feraient instinctivement Foo f = Foo("hello");. Je pense qu'ils se fâcheraient si cela échouait. Ma solution l'a initialement rejetée (et des cas très similaires) avec une exception / affirmation-échec et quelqu'un s'est plaint.
Johannes Schaub - litb
@ JohannesSchaub-litb Oui, OP veut interdire de rejeter la valeur générée par un constructeur en forçant les liaisons. Mon exemple est faux.
didierc
7

Vous n'avez tout simplement pas de constructeur par défaut et vous avez besoin d'une référence à une instance dans chaque constructeur.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

struct S
{
    S( SelfRef, S const & ) {}
};

int main()
{
    S a( selfRef, a );
}
Bravo et hth. - Alf
la source
3
Bonne idée, mais dès que vous avez une variable: S(selfRef, a);. : /
Xeo
3
@Xeo S(SelfRef, S const& s) { assert(&s == this); }, si une erreur d'exécution est acceptable.
6

Non, j'ai bien peur que ce ne soit pas possible. Mais vous pouvez obtenir le même effet en créant une macro.

#define FOO(x) Foo _foo(x)

Avec cela en place, vous pouvez simplement écrire FOO (x) au lieu de Foo my_foo (x).

amaurée
la source
5
J'allais voter pour, mais ensuite j'ai vu "vous pourriez créer une macro".
Griwes
1
Ok, j'ai corrigé les traits de soulignement. @Griwes - Ne soyez pas fondamentaliste. Il vaut mieux dire "utiliser une macro" que "cela ne peut pas être fait".
amaurea
5
Eh bien, cela ne peut pas être fait. Vous n'avez pas du tout résolu le problème, c'est toujours parfaitement légal de le faire Foo();.
Chiot
11
Maintenant, vous êtes têtu ici. Renommez la classe Foo en quelque chose de compliqué et appelez la macro Foo. Problème résolu.
amaurea
8
Quelque chose comme:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley
4

Puisque l'objectif principal est d'éviter les bogues, considérez ceci:

struct Foo
{
  Foo( const char* ) { /* ... */ }
};

enum { Foo };

int main()
{
  struct Foo foo( "hi" ); // OK
  struct Foo( "hi" ); // fail
  Foo foo( "hi" ); // fail
  Foo( "hi" ); // fail
}

De cette façon, vous ne pouvez pas oublier de nommer la variable et vous ne pouvez pas oublier d'écrire struct. Verbose, mais sûr.

Daniel Frey
la source
1

Déclarez un constructeur paramétrique comme explicite et personne ne créera jamais un objet de cette classe sans le vouloir.

Par exemple

class Foo
{
public: 
  explicit Foo(const char*);
};

void fun(const Foo&);

ne peut être utilisé que de cette façon

void g() {
  Foo a("text");
  fun(a);
}

mais jamais de cette façon (via un temporaire sur la pile)

void g() {
  fun("text");
}

Voir aussi: Alexandrescu, C ++ Coding Standards, Item 40.

stefan.gal
la source
3
Cela permet fun(Foo("text"));.
Guilherme Bernal