Come vietare i provvisori


107

Per una classe Foo, c'è un modo per impedirne la costruzione senza dargli un nome?

Per esempio:

Foo("hi");

E lo consenti solo se gli dai un nome, come il seguente?

Foo my_foo("hi");

La durata del primo è solo l'affermazione e la seconda è il blocco che lo racchiude. Nel mio caso d'uso, Foomisura il tempo tra il costruttore e il distruttore. Dato che non mi riferisco mai alla variabile locale, spesso dimentico di inserirla e cambio accidentalmente la durata. Vorrei invece ricevere un errore in fase di compilazione.


8
Questo potrebbe anche tornare utile per le protezioni di blocco mutex.
lucas clemente

1
Beh, si potrebbe scrivere il proprio compilatore C ++ in cui era proibito, ma a rigor di termini non sarebbe quindi C ++. Ci sono anche luoghi in cui i provvisori come quello sarebbero utili, come quando si restituisce un oggetto da una funzione per esempio (come return std::string("Foo");)
Un tizio programmatore

2
No, non puoi farlo, mi dispiace
Armen Tsirunyan

2
A seconda della tua religione, questo potrebbe essere un caso in cui le macro potrebbero tornare utili (utilizzando quel tipo sempre e solo tramite una macro che crea sempre una variabile)
PlasmaHH

3
Sembra più qualcosa che vorrei che il mio strumento LINT catturi che qualcosa che vorrei prevenire sintatticamente da un hack del compilatore.
Warren P

Risposte:


101

Un'altra soluzione basata su macro:

#define Foo class Foo

L'affermazione si Foo("hi");espande a class Foo("hi");, che è mal formata; ma si Foo a("hi")espande in class Foo a("hi"), il che è corretto.

Questo ha il vantaggio di essere compatibile sia dal codice sorgente che dal binario con il codice esistente (corretto). (Questa affermazione non è del tutto corretta - si prega di vedere il commento di Johannes Schaub e la successiva discussione di seguito: "Come puoi sapere che è compatibile con il codice esistente? Il suo amico include la sua intestazione e ha void f () {int Foo = 0;} che in precedenza veniva compilato correttamente e ora viene compilato male! Inoltre, ogni riga che definisce una funzione membro della classe Foo fallisce: void class Foo :: bar () {} " )


51
Come puoi sapere che è compatibile con il codice sorgente? Il suo amico include la sua intestazione e ha void f() { int Foo = 0; }che precedentemente compilata bene e ora non si compila correttamente! Inoltre, ogni linea che definisce una funzione membro della classe Foo fallisce: void class Foo::bar() {}.
Johannes Schaub - litb

21
Come può ottenere così tanti voti? Basta guardare il commento di @ JohannesSchaub-litb e capirai che questa è davvero una pessima soluzione. Perché tutte le definizioni delle funzioni membro non sono valide dopo questo .. -1 da parte mia
Aamir

2
@JustMaximumPower: spero che sia stato sarcastico perché in caso contrario, è ancora una brutta (leggi peggio) soluzione alternativa. Perché siamo tornati al punto di partenza dopo averlo indefinito, il che significa che non riceverai un errore di compilazione (che l'OP intendeva) su una riga simile, ad esempio Foo("Hi")all'interno di Foo.cpp ora
Aamir

1
@Aamir No dico sul serio. Martin C. Martin intende usarlo per proteggere l'uso di Foo e non l'implementazione.
JustMaximumPower

1
Ho provato in Visual Studio 2012 e ho trovato che class Foo("hi");va bene per la compilazione.
fresky

71

Che ne dici di un piccolo trucco

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

void Foo (float);


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

1
Great Hack! Una nota: anche Foo a("hi");(senza class) sarebbe un errore.
maschera di bit

Non sono sicuro di capire. Foo ("ciao") prova a chiamare void Foo (float) e il risultato è un errore del linker? Ma perché viene chiamata la versione float al posto del Foo ctor?
fino al

2
undu, hm quale compilatore stai usando? gcc 3.4 si lamenta del fatto che non c'è conversione in float. Prova a chiamare una funzione Fooperché ha la precedenza su una classe.

@aleguna in realtà non ho provato a eseguire questo codice, era solo una (cattiva) ipotesi: s Ma hai risposto comunque alla mia domanda, non sapevo che la funzione avesse la precedenza sulla classe.
fino al

1
@didierc no, non Foo::Foo("hi")è consentito in C ++.
Johannes Schaub - litb

44

Rendi privato il costruttore ma dai alla classe un metodo create .


9
-1: Come risolve il problema dell'OP? È ancora possibile scrivere Foo::create();sopraFoo const & x = Foo::create();
Thomas Eding

@ThomasEding Immagino che tu abbia ragione, non risolve il problema principale di OP, ma lo costringe solo a pensare e a non commettere l'errore che sta facendo.
dchhetri

1
@ThomasEding non puoi proteggerti dagli utenti arrabbiati che vogliono rompere il sistema. Anche con l'hack di @ ecatmur puoi dire std::common_type<Foo>::type()e ottieni un file temporaneo. O anche typedef Foo bar; bar().
Johannes Schaub - litb

@ JohannesSchaub-litb: Ma la grande differenza è se è stato o meno per errore. Non c'è quasi modo che la digitazione avvenga std::common_type<Foo>::type()per errore. Tralasciare Foo const & x = ...per sbaglio è del tutto credibile.
Thomas Eding

24

Questo non si traduce in un errore del compilatore, ma in un errore di runtime. Invece di misurare un tempo sbagliato, ottieni un'eccezione che potrebbe essere anche accettabile.

Qualsiasi costruttore che si desidera proteggere necessita di un argomento predefinito su cui set(guard)viene chiamato.

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

Le caratteristiche sono:

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();
}

Il caso f2, f3e il ritorno di "hello"non essere voluto. Per evitare il lancio, puoi consentire che l'origine di una copia sia temporanea, reimpostando il guardper proteggerci ora anziché l'origine della copia. Ora capisci anche perché abbiamo usato i puntatori sopra: ci consentono di essere flessibili.

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

Le caratteristiche per f2, f3e per return "hello"ora sono sempre // OK.


2
Foo f = "hello"; // may throwQuesto è sufficiente per spaventarmi a non usare mai questo codice.
Thomas Eding

4
@ Thomas, consiglio di contrassegnare il costruttore explicite quindi tale codice non viene più compilato. l'obiettivo era quello di fare un'offerta temporanea, e lo fa. se hai paura, puoi fare in modo che non venga lanciato impostando l'origine di una copia nel costruttore di copia o di spostamento come non temporaneo. allora solo l'oggetto finale di più copie può essere lanciato se finisce ancora come temporaneo.
Johannes Schaub - litb

2
Mio Dio. Non sono un principiante in C ++ e C ++ 11, ma non riesco a capire come funziona. Potresti aggiungere un paio di spiegazioni? ..
Mikhail

6
@Mikhail l'ordine di distruzione degli oggetti temporanei che vengono distrutti negli stessi punti è l'ordine inverso della loro costruzione. L'argomento predefinito che passa il chiamante è temporaneo. Se anche l' Foooggetto è temporaneo e la sua durata termina con la stessa espressione dell'argomento predefinito, il Foodtor dell'oggetto verrà richiamato prima del dtor dell'argomento predefinito, perché il primo è stato creato dopo il secondo.
Johannes Schaub - litb

1
@ JohannesSchaub-litb Trucco molto bello. Ho davvero pensato che fosse impossibile distinguere Foo(...);e Foo foo(...);dall'interno del file Foo.
Mikhail

18

Qualche anno fa ho scritto una patch per il compilatore GNU C ++ che aggiunge una nuova opzione di avviso per quella situazione. Questo è tracciato in un oggetto Bugzilla .

Sfortunatamente, GCC Bugzilla è un cimitero dove i suggerimenti sulle funzionalità incluse nelle patch ben ponderate vanno a morire. :)

Ciò è stato motivato dal desiderio di cogliere esattamente il tipo di bug oggetto di questa domanda in un codice che utilizza oggetti locali come gadget per il blocco e lo sblocco, la misurazione del tempo di esecuzione e così via.


9

Così com'è, con la tua implementazione, non puoi farlo, ma puoi usare questa regola a tuo vantaggio:

Gli oggetti temporanei non possono essere associati a riferimenti non costanti

È possibile spostare il codice dalla classe a una funzione indipendente che accetta un parametro di riferimento non const. Se lo fai, riceverai un errore del compilatore se un temporaneo tenta di collegarsi al riferimento non const.

Esempio di codice

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

Produzione

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&)’

1
@didierc: a condizione che forniscano una funzione aggiuntiva, sta a te non farlo. Stiamo cercando di modificare un modo per ottenere qualcosa non esplicitamente consentito dallo standard, quindi ovviamente ci saranno delle restrizioni.
Alok Save

@didierc il parametro xè un oggetto con nome quindi non è chiaro se vogliamo davvero proibirlo. Se il costruttore che avresti usato fosse esplicito, forse le persone lo farebbero istintivamente Foo f = Foo("hello");. Penso che si arrabbierebbero se fallisse. La mia soluzione inizialmente l'ha rifiutata (e casi molto simili) con un'eccezione / asserzione-fallimento e qualcuno si è lamentato.
Johannes Schaub - litb

@ JohannesSchaub-litb Sì, OP vuole vietare di scartare il valore generato da un costruttore forzando le associazioni. Il mio esempio è sbagliato.
didierc

7

Semplicemente non hai un costruttore predefinito e richiede un riferimento a un'istanza in ogni costruttore.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

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

int main()
{
    S a( selfRef, a );
}

3
Bella idea, ma non appena si dispone di una variabile: S(selfRef, a);. : /
Xeo

3
@Xeo S(SelfRef, S const& s) { assert(&s == this); }, se un errore di runtime è accettabile.

6

No, temo che non sia possibile. Ma potresti ottenere lo stesso effetto creando una macro.

#define FOO(x) Foo _foo(x)

Con questo a posto, puoi semplicemente scrivere FOO (x) invece di Foo my_foo (x).


5
Stavo per votare, ma poi ho visto "potresti creare una macro".
Griwes

1
Ok, risolti i trattini bassi. @ Griwes - Non essere un fondamentalista. È meglio dire "usa una macro" piuttosto che "non si può fare".
amaurea

5
Beh, non si può fare. Non hai risolto affatto il problema, è ancora perfettamente legale farlo Foo();.
Cucciolo

11
Ora sei testardo qui. Rinomina la classe Foo in modo complicato e chiama la macro Foo. Problema risolto.
amaurea

8
Qualcosa del tipo:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley

4

Poiché l'obiettivo principale è prevenire i bug, considera questo:

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
}

In questo modo non puoi dimenticare di nominare la variabile e non puoi dimenticare di scrivere struct. Verboso, ma sicuro.


1

Dichiara il costruttore uniparametrico come esplicito e nessuno creerà mai un oggetto di quella classe involontariamente.

Per esempio

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

void fun(const Foo&);

può essere utilizzato solo in questo modo

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

ma mai in questo modo (tramite un temporaneo in pila)

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

Vedi anche: Alexandrescu, C ++ Coding Standards, Item 40.


3
Questo consente fun(Foo("text"));.
Guilherme Bernal
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.