Wie man Provisorien verbietet

107

Gibt es für eine Klasse Foo eine Möglichkeit, das Konstruieren zu verbieten, ohne ihr einen Namen zu geben?

Beispielsweise:

Foo("hi");

Und erlauben Sie es nur, wenn Sie ihm einen Namen geben, wie den folgenden?

Foo my_foo("hi");

Die Lebensdauer des ersten ist nur die Aussage, und die zweite ist der umschließende Block. In meinem Anwendungsfall Foowird die Zeit zwischen Konstruktor und Destruktor gemessen. Da ich mich nie auf die lokale Variable beziehe, vergesse ich oft, sie einzugeben, und ändere versehentlich die Lebensdauer. Ich möchte stattdessen einen Fehler bei der Kompilierung erhalten.

Martin C. Martin
quelle
8
Dies könnte auch für Mutex-Schlossschützer nützlich sein.
Lucas Clemente
1
Nun, Sie könnten Ihren eigenen C ++ - Compiler schreiben, wo es verboten war, aber genau genommen wäre es dann nicht C ++. Es gibt auch Orte, an denen solche Provisorien nützlich wären, wie zum Beispiel beim Zurückgeben eines Objekts von einer Funktion (wie return std::string("Foo");)
Ein Programmierer-Typ
2
Nein, das können Sie nicht, sorry
Armen Tsirunyan
2
Abhängig von Ihrer Religion kann dies ein Fall sein, in dem Makros nützlich sein können (indem Sie diesen Typ immer nur über ein Makro verwenden, das immer eine Variable erstellt)
PlasmaHH
3
Scheint eher etwas zu sein, das mein LINT-Tool abfangen soll, als etwas, das ich durch einen Compiler-Hack syntaktisch verhindern möchte.
Warren P

Antworten:

101

Eine weitere makrobasierte Lösung:

#define Foo class Foo

Die Aussage Foo("hi");erweitert sich zu class Foo("hi");, was schlecht geformt ist; aber Foo a("hi")erweitert sich class Foo a("hi"), was richtig ist.

Dies hat den Vorteil, dass es sowohl Quell- als auch Binärkompatibel mit vorhandenem (korrektem) Code ist. (Diese Behauptung ist nicht ganz richtig - siehe Johannes Schaubs Kommentar und die anschließende Diskussion unten: "Woher wissen Sie, dass die Quelle mit dem vorhandenen Code kompatibel ist? Sein Freund enthält seinen Header und hat void f () {int Foo = 0;} Die Zeile, die eine Elementfunktion der Klasse Foo definiert, schlägt fehl: void class Foo :: bar () {} " )

ecatmur
quelle
51
Wie können Sie wissen, dass es mit vorhandenem Code quellkompatibel ist? Sein Freund enthält seinen Header und hat void f() { int Foo = 0; }den zuvor gut kompiliert und jetzt falsch kompiliert! Außerdem schlägt jede Zeile, die eine Elementfunktion der Klasse Foo definiert, fehl : void class Foo::bar() {}.
Johannes Schaub - litb
21
Wie kann das so viele Stimmen bekommen? Schauen Sie sich einfach den Kommentar von @ JohannesSchaub-litb an und Sie werden verstehen, dass dies eine wirklich schlechte Lösung ist. Weil alle Definitionen der Mitgliedsfunktionen danach ungültig sind. -1 von meiner Seite
Aamir
2
@ JustMaximumPower: Ich hoffe, das war sarkastisch, denn wenn nicht, ist es wieder eine schlechte (schlechter lesen) Problemumgehung. Weil wir nach dem Aufheben der Definition wieder auf dem ersten Platz sind, was bedeutet, dass Sie keinen Kompilierungsfehler (den das OP beabsichtigt hat) in einer ähnlichen Zeile erhalten, Foo("Hi")dh jetzt in Foo.cpp
Aamir
1
@ Amir Nein, ich meine es ernst. Martin C. Martin beabsichtigt, es zu verwenden, um die Verwendung von Foo und nicht die Implementierung zu schützen.
JustMaximumPower
1
Ich habe es in Visual Studio 2012 versucht und festgestellt, class Foo("hi");dass das Kompilieren in Ordnung ist.
Fresky
71

Wie wäre es mit einem kleinen Hack

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

void Foo (float);


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

quelle
1
Toller Hack! Ein Hinweis: Foo a("hi");(ohne class) wäre auch ein Fehler.
Bitmaske
Ich bin mir nicht sicher ob ich das verstehe. Foo ("Hallo") versucht void Foo (float) aufzurufen und führt zu einem Linkerfehler? Aber warum heißt die float-Version anstelle des Foo ctor?
Undu
2
undu, hm welchen compiler benutzt du? gcc 3.4 beschwert sich, dass es keine Umwandlung in float gibt. Es wird versucht, eine Funktion aufzurufen, Fooda sie Vorrang vor einer Klasse hat.
@aleguna Eigentlich habe ich nicht versucht, diesen Code auszuführen, das war nur eine (schlechte) Vermutung: s Aber du hast meine Frage trotzdem beantwortet, ich wusste nicht, dass Funktion Vorrang vor Klasse hat.
Undu
1
@didierc nein, Foo::Foo("hi")ist in C ++ nicht erlaubt .
Johannes Schaub - Litb
44

Machen Sie den Konstruktor privat, aber geben Sie der Klasse eine Erstellungsmethode .

dchhetri
quelle
9
-1: Wie löst dies das Problem des OP überhaupt? Sie können immer noch Foo::create();überFoo const & x = Foo::create();
Thomas Eding
@ThomasEding Ich denke, Sie haben Recht, es behebt nicht das Kernproblem von OP, sondern zwingt ihn nur zum Nachdenken und macht nicht den Fehler, den er macht.
Dchhetri
1
@ThomasEding Sie können sich nicht vor verärgerten Benutzern schützen, die das System beschädigen möchten. Selbst mit @ ecatmurs Hack kann man sagen std::common_type<Foo>::type()und man bekommt eine vorübergehende. Oder sogar typedef Foo bar; bar().
Johannes Schaub - Litb
@ JohannesSchaub-litb: Aber der große Unterschied ist, ob es versehentlich war oder nicht. Es gibt fast keine Möglichkeit, versehentlich zu tippen std::common_type<Foo>::type(). Das versehentlich wegzulassen Foo const & x = ...ist absolut glaubwürdig.
Thomas Eding
24

Dieser führt nicht zu einem Compilerfehler, sondern zu einem Laufzeitfehler. Anstatt eine falsche Zeit zu messen, erhalten Sie eine Ausnahme, die möglicherweise auch akzeptabel ist.

Jeder Konstruktor, den Sie schützen möchten, benötigt ein Standardargument, für das set(guard)aufgerufen wird.

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

Die Eigenschaften sind:

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

Der Fall f2, f3und die Rückkehr der "hello"nicht gewollt werden. Um ein Werfen zu verhindern, können Sie zulassen, dass die Quelle einer Kopie vorübergehend ist, indem Sie die Option " guardJetzt schützen" anstelle der Quelle der Kopie zurücksetzen . Jetzt sehen Sie auch, warum wir die obigen Zeiger verwendet haben - es ermöglicht uns, flexibel zu sein.

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

Die Eigenschaften für f2, f3und return "hello"sind jetzt immer // OK.

Johannes Schaub - litb
quelle
2
Foo f = "hello"; // may throwDies ist genug, um mich zu erschrecken, diesen Code niemals zu verwenden.
Thomas Eding
4
@ Thomas, ich empfehle, den Konstruktor zu markieren explicitund dann solchen Code nicht mehr zu kompilieren. Das Ziel war es, das Temporäre zu fotografieren, und das tut es auch. Wenn Sie Angst haben, können Sie dafür sorgen, dass es nicht geworfen wird, indem Sie die Quelle einer Kopie in der Kopie festlegen oder den Konstruktor so verschieben, dass er nicht vorübergehend ist. dann darf nur das endgültige Objekt mehrerer Kopien geworfen werden, wenn es noch als temporär endet.
Johannes Schaub - litb
2
Mein Gott. Ich bin kein Anfänger in C ++ und C ++ 11, aber ich kann nicht verstehen, wie das funktioniert. Könnten Sie bitte ein paar Erklärungen hinzufügen? ..
Mikhail
6
@Mikhail Die Reihenfolge der Zerstörung von temporären Objekten, die an denselben Punkten zerstört werden, ist die umgekehrte Reihenfolge ihrer Konstruktion. Das Standardargument, das der Anrufer übergibt, ist vorübergehend. Wenn das FooObjekt ebenfalls temporär ist und seine Lebensdauer mit demselben Ausdruck wie das Standardargument endet, wird der Foodtor des Objekts vor dem dtor des Standardarguments aufgerufen, da der erstere nach dem letzteren erstellt wurde.
Johannes Schaub - Litb
1
@ JohannesSchaub-litb Sehr schöner Trick. Ich dachte wirklich, es ist unmöglich zu unterscheiden Foo(...);und Foo foo(...);von innen Foo.
Mikhail
18

Vor einigen Jahren habe ich einen Patch für den GNU C ++ - Compiler geschrieben, der eine neue Warnoption für diese Situation hinzufügt. Dies wird in einem Bugzilla-Artikel verfolgt .

Leider ist GCC Bugzilla eine Grabstätte, auf der wohlüberlegte Vorschläge für Patch-Features sterben. :) :)

Dies wurde durch den Wunsch motiviert, genau die Art von Fehlern zu erkennen, die Gegenstand dieser Frage im Code sind, der lokale Objekte als Gadgets zum Sperren und Entsperren, Messen der Ausführungszeit usw. verwendet.

Kaz
quelle
9

Bei Ihrer Implementierung können Sie dies nicht tun, aber Sie können diese Regel zu Ihrem Vorteil verwenden:

Temporäre Objekte können nicht an nicht konstante Referenzen gebunden werden

Sie können den Code aus der Klasse in eine freistehende Funktion verschieben, die einen nicht konstanten Referenzparameter verwendet. In diesem Fall wird ein Compilerfehler angezeigt, wenn ein temporärer Benutzer versucht, eine Bindung an die nicht konstante Referenz herzustellen.

Codebeispiel

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

Ausgabe

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 Speichern
quelle
1
@didierc: Vorausgesetzt, sie bieten eine zusätzliche Funktion. Es liegt an Ihnen, dies nicht zu tun. Wir versuchen, einen Weg zu finden, um etwas zu erreichen, das vom Standard nicht ausdrücklich zugelassen wird, daher gibt es natürlich Einschränkungen.
Alok Save
@didierc Der Parameter xist ein benanntes Objekt, daher ist nicht klar, ob wir es wirklich verbieten wollen. Wenn der Konstruktor, den Sie verwendet hätten, explizit ist, könnten die Leute dies instinktiv tun Foo f = Foo("hello");. Ich denke, sie würden wütend werden, wenn es fehlschlagen würde. Meine Lösung lehnte es zunächst (und in sehr ähnlichen Fällen) mit einer Ausnahme / einem Assert-Fehler ab, und jemand beschwerte sich.
Johannes Schaub - Litb
@ JohannesSchaub-litb Ja, OP möchte verbieten, den von einem Konstruktor generierten Wert durch Erzwingen von Bindungen zu verwerfen. Mein Beispiel ist falsch.
Didierc
7

Sie haben einfach keinen Standardkonstruktor und benötigen in jedem Konstruktor einen Verweis auf eine Instanz.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

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

int main()
{
    S a( selfRef, a );
}
Prost und hth. - Alf
quelle
3
Gute Idee, aber sobald Sie eine Variable haben : S(selfRef, a);. : /
Xeo
3
@Xeo S(SelfRef, S const& s) { assert(&s == this); }, wenn ein Laufzeitfehler akzeptabel ist.
6

Nein, ich fürchte, das ist nicht möglich. Sie können jedoch den gleichen Effekt erzielen, indem Sie ein Makro erstellen.

#define FOO(x) Foo _foo(x)

Wenn dies vorhanden ist, können Sie einfach FOO (x) anstelle von Foo my_foo (x) schreiben.

Amaurea
quelle
5
Ich wollte upvoten, aber dann sah ich "Sie könnten ein Makro erstellen".
Griwes
1
Ok, die Unterstriche wurden behoben. @Griwes - Sei kein Fundamentalist. Es ist besser zu sagen "benutze ein Makro" als "das geht nicht".
Amaurea
5
Nun, das geht nicht. Sie haben das Problem überhaupt nicht gelöst, es ist immer noch völlig legal Foo();.
Welpe
11
Jetzt bist du hier stur. Benennen Sie die Foo-Klasse etwas kompliziert um und rufen Sie das Makro Foo auf. Problem gelöst.
Amaurea
8
So etwas wie:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley
4

Beachten Sie Folgendes, da das Hauptziel darin besteht, Fehler zu vermeiden:

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
}

Auf diese Weise können Sie nicht vergessen, die Variable zu benennen, und Sie können nicht vergessen, zu schreiben struct. Ausführlich, aber sicher.

Daniel Frey
quelle
1

Deklarieren Sie einen einparametrischen Konstruktor als explizit, und niemand wird jemals ungewollt ein Objekt dieser Klasse erstellen.

Beispielsweise

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

void fun(const Foo&);

kann nur so verwendet werden

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

aber niemals so (durch eine temporäre auf dem Stapel)

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

Siehe auch: Alexandrescu, C ++ Coding Standards, Punkt 40.

stefan.gal
quelle
3
Dies erlaubt fun(Foo("text"));.
Guilherme Bernal