Jak zabronić używania tymczasowych

107

Czy w przypadku klasy Foo istnieje sposób, aby zabronić jej tworzenia bez nadawania jej nazwy?

Na przykład:

Foo("hi");

I zezwolić na to tylko wtedy, gdy nadasz mu nazwę, taką jak ta?

Foo my_foo("hi");

Czas życia pierwszego to tylko instrukcja, a drugi to otaczający blok. W moim przypadku Foojest to pomiar czasu między konstruktorem a destruktorem. Ponieważ nigdy nie odwołuję się do zmiennej lokalnej, często zapominam o jej wstawieniu i przypadkowo zmieniam czas życia. Chciałbym zamiast tego otrzymać błąd czasu kompilacji.

Martin C. Martin
źródło
8
Może się to również przydać w przypadku osłon zamków mutex.
lucas clemente
1
Cóż, mógł napisać własny kompilator C ++, gdzie został zakazany, ale ściśle mówiąc nie byłoby C ++ wtedy. Są też miejsca, w których takie tymczasowe byłyby przydatne, na przykład przy zwracaniu obiektu z funkcji (np. return std::string("Foo");)
Jakiś programista koleś
2
Nie, nie możesz tego zrobić, przepraszam
Armen Tsirunyan
2
W zależności od religii może to być przypadek, w którym makra mogą się przydać (używając tego typu tylko za pomocą makra, które zawsze tworzy zmienną)
PlasmaHH
3
Wygląda bardziej na coś, co chciałbym, aby moje narzędzie LINT przechwyciło, niż coś, czemu chciałbym zapobiec składniowo przez włamanie do kompilatora.
Warren P

Odpowiedzi:

101

Inne rozwiązanie oparte na makrach:

#define Foo class Foo

Oświadczenie Foo("hi");rozszerza się do class Foo("hi");, które jest źle sformułowane; ale Foo a("hi")rozszerza się class Foo a("hi"), co jest poprawne.

Ma to tę zaletę, że jest zgodny zarówno z kodem źródłowym, jak i binarnym z istniejącym (poprawnym) kodem. (To twierdzenie nie jest całkowicie poprawne - zobacz komentarz Johannesa Schauba i następującą dyskusję poniżej: „Skąd możesz wiedzieć, że jest on zgodny z istniejącym kodem? Jego przyjaciel zawiera swój nagłówek i ma void f () {int Foo = 0;} który wcześniej dobrze się skompilował, a teraz źle się kompiluje! Ponadto każda linia definiująca funkcję składową klasy Foo kończy się niepowodzeniem: void class Foo :: bar () {} " )

ecatmur
źródło
51
Skąd możesz wiedzieć, że jest on zgodny z istniejącym kodem? Jego przyjaciel zawiera swój nagłówek, void f() { int Foo = 0; }który wcześniej dobrze skompilował, a teraz źle się kompiluje! Ponadto, każda linia, która definiuje funkcji składowej klasy Foo nie: void class Foo::bar() {}.
Johannes Schaub - litb
21
Jak to może zdobyć tyle głosów? Wystarczy spojrzeć na komentarz @ JohannesSchaub-litb, a zrozumiesz, że to naprawdę złe rozwiązanie. Ponieważ wszystkie definicje funkcji składowych są po tym nieważne .. -1 z mojej strony
Aamir
2
@JustMaximumPower: Mam nadzieję, że było to sarkastyczne, ponieważ jeśli nie, jest to znowu złe (czytaj gorsze) obejście. Ponieważ wróciliśmy do punktu wyjścia po niezdefiniowaniu go, co oznacza, że ​​nie otrzymasz błędu kompilacji (co zamierzał OP) w podobnej linii, tj. Foo("Hi")Teraz wewnątrz Foo.cpp
Aamir
1
@Aamir Nie, mówię poważnie. Martin C. Martin zamierza użyć go do ochrony użycia Foo, a nie implementacji.
JustMaximumPower
1
Próbowałem w programie Visual Studio 2012 i stwierdziłem, że class Foo("hi");jest w porządku do kompilacji.
fresky
71

Co powiesz na mały hack

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

void Foo (float);


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

źródło
1
Świetny hack! Jedna uwaga: Foo a("hi");(bez class) też byłby błąd.
maska ​​bitów
Nie jestem pewien, czy rozumiem. Foo ("hello") próbuje wywołać void Foo (float) i powoduje to błąd konsolidatora? Ale dlaczego zamiast elementu Foo ctor jest wywoływana wersja zmiennoprzecinkowa?
undu
2
undu, hm jakiego kompilatora używasz? gcc 3.4 narzeka, że ​​nie ma konwersji do typu float. Próbuje wywołać funkcję, Fooponieważ ma ona pierwszeństwo przed klasą.
@aleguna właściwie nie próbowałem uruchamiać tego kodu, to był tylko (zły) przypuszczenie: s Ale i tak odpowiedziałeś na moje pytanie, nie wiedziałem, że funkcja ma pierwszeństwo przed klasą.
undu
1
@didierc no, Foo::Foo("hi")jest niedozwolone w C ++.
Johannes Schaub - litb
44

Ustaw konstruktor jako prywatny, ale nadaj klasie metodę tworzenia .

dchhetri
źródło
9
-1: Jak to w ogóle rozwiązuje problem PO? Nadal można napisać Foo::create();nadFoo const & x = Foo::create();
Thomas Eding
@ThomasEding Myślę, że masz rację, nie rozwiązuje to podstawowego problemu OP, ale po prostu zmusza go do myślenia i nie popełnienia błędu, który popełnia.
dchhetri
1
@ThomasEding nie możesz się zabezpieczyć przed wściekłymi użytkownikami, którzy chcą złamać system. Nawet z hackiem @ ecatmur możesz powiedzieć, std::common_type<Foo>::type()a otrzymasz tymczasowy. Albo nawet typedef Foo bar; bar().
Johannes Schaub - litb
@ JohannesSchaub-litb: Ale duża różnica polega na tym, czy był to przypadek, czy nie. Prawie nie ma mowy o wpisaniu std::common_type<Foo>::type()przez pomyłkę. Pomijanie tego Foo const & x = ...przez przypadek jest całkowicie wiarygodne.
Thomas Eding
24

Ten nie powoduje błędu kompilatora, ale błąd wykonania. Zamiast mierzyć niewłaściwy czas, otrzymujesz wyjątek, który również może być akceptowalny.

Każdy konstruktor, którego chcesz chronić, potrzebuje domyślnego argumentu, który set(guard)jest wywoływany.

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

Cechy charakterystyczne to:

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

Przypadek f2, f3a powrót "hello"nie może być chciał. Aby zapobiec rzucaniu, możesz pozwolić, aby źródło kopii było tymczasowe, resetując, guardaby teraz chroniło nas zamiast źródła kopii. Teraz widzisz również, dlaczego użyliśmy powyższych wskaźników - pozwala nam to być elastycznym.

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

Cechy charakterystyczne dla f2, f3i return "hello"są teraz zawsze // OK.

Johannes Schaub - litb
źródło
2
Foo f = "hello"; // may throwTo wystarczy, żebym przestraszył mnie, żebym nigdy nie używał tego kodu.
Thomas Eding
4
@thomas, polecam zaznaczyć konstruktora expliciti wtedy taki kod już się nie kompiluje. celem było unieważnienie tego, co tymczasowe, i tak jest. jeśli się boisz, możesz sprawić, że nie wyrzuci, ustawiając źródło kopii w konstruktorze kopiowania lub przenoszenia na nienowoczesne. wtedy tylko ostatni obiekt z kilku kopii może zostać rzucony, jeśli nadal kończy się jako tymczasowy.
Johannes Schaub - litb
2
Mój Boże. Nie jestem nowicjuszem w C ++ i C ++ 11, ale nie rozumiem, jak to działa. Czy mógłbyś dodać kilka wyjaśnień? ..
Michaił
6
@Mikhail kolejność niszczenia tymczasowych obiektów, które są niszczone w tych samych punktach, jest odwrotną kolejnością ich budowy. Domyślny argument, który przekazuje wywołujący, jest tymczasowy. Jeśli Fooobiekt jest również tymczasowy, a jego żywotność kończy się tym samym wyrażeniem, co argument domyślny, wówczas Foodtor obiektu zostanie wywołany przed dtor argumentu domyślnego, ponieważ pierwszy został utworzony po drugim.
Johannes Schaub - litb
1
@ JohannesSchaub-litb Bardzo fajna sztuczka. Naprawdę myślałem, że nie da się odróżnić Foo(...);i Foo foo(...);od wewnątrz Foo.
Michaił
18

Kilka lat temu napisałem łatkę dla kompilatora GNU C ++, która dodaje nową opcję ostrzeżenia o takiej sytuacji. Jest to śledzone w elemencie Bugzilli .

Niestety GCC Bugzilla to cmentarzysko, w którym dobrze przemyślane sugestie dotyczące funkcji zawartych w łatkach umierają. :)

Było to motywowane chęcią wyłapania dokładnie tego rodzaju błędów, które są przedmiotem tego pytania, w kodzie, który wykorzystuje lokalne obiekty jako gadżety do blokowania i odblokowywania, mierzenia czasu wykonania i tak dalej.

Kaz
źródło
9

W przypadku Twojej implementacji nie możesz tego zrobić, ale możesz użyć tej reguły na swoją korzyść:

Obiekty tymczasowe nie mogą być powiązane z odniesieniami innymi niż stałe

Możesz przenieść kod z klasy do wolnostojącej funkcji, która przyjmuje parametr odniesienia inny niż const. Jeśli to zrobisz, otrzymasz błąd kompilatora, jeśli tymczasowy spróbuje powiązać się z odwołaniem innym niż const.

Przykład kodu

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

Wynik

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
źródło
1
@didierc: Pod warunkiem, że zapewniają dodatkową funkcję.To zależy od Ciebie, aby tego nie robić.
Alok Save
@didierc parametr xjest nazwanym obiektem, więc nie jest jasne, czy naprawdę chcemy tego zabronić. Jeśli konstruktor, którego użyłeś, jest jawny, może ludzie zrobiliby to instynktownie Foo f = Foo("hello");. Myślę, że byliby źli, gdyby to się nie udało. Moje rozwiązanie początkowo odrzuciło to (i bardzo podobne przypadki) z wyjątkiem / niepowodzeniem potwierdzenia i ktoś narzekał.
Johannes Schaub - litb
@ JohannesSchaub-litb Tak, OP chce zabronić odrzucania wartości wygenerowanej przez konstruktora przez wymuszenie powiązań. Mój przykład jest zły.
didierc
7

Po prostu nie mają domyślnego konstruktora i wymagają odwołania do wystąpienia w każdym konstruktorze.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

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

int main()
{
    S a( selfRef, a );
}
Pozdrawiam i hth. - Alf
źródło
3
Fajny pomysł, ale tak szybko, jak masz jedną zmienną: S(selfRef, a);. : /
Xeo
3
@Xeo S(SelfRef, S const& s) { assert(&s == this); }, jeśli błąd wykonania jest akceptowalny.
6

Nie, obawiam się, że to niemożliwe. Ale możesz uzyskać ten sam efekt, tworząc makro.

#define FOO(x) Foo _foo(x)

Mając to na miejscu, możesz po prostu napisać FOO (x) zamiast Foo my_foo (x).

amaurea
źródło
5
Chciałem głosować za, ale wtedy zobaczyłem, że „możesz stworzyć makro”.
Griwes
1
Ok, poprawiono podkreślenia. @Griwes - Nie bądź fundamentalistą. Lepiej jest powiedzieć „użyj makra” niż „tego nie da się zrobić”.
amaurea
5
Cóż, nie da się tego zrobić. W ogóle nie rozwiązałeś problemu, nadal jest to całkowicie legalne Foo();.
Puppy
11
Teraz jesteś tutaj uparty. Zmień nazwę klasy Foo na coś skomplikowanego i wywołaj makro Foo. Problem rozwiązany.
amaurea
8
Coś w stylu:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley
4

Ponieważ głównym celem jest zapobieganie błędom, rozważ to:

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
}

W ten sposób nie możesz zapomnieć o nazwie zmiennej i nie możesz zapomnieć o napisaniu struct. Szczegółowy, ale bezpieczny.

Daniel Frey
źródło
1

Zadeklaruj jednoparametryczny konstruktor jako jawny i nikt nigdy nie utworzy przypadkowo obiektu tej klasy.

Na przykład

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

void fun(const Foo&);

można używać tylko w ten sposób

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

ale nigdy w ten sposób (przez tymczasowe na stosie)

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

Zobacz też: Alexandrescu, Standardy kodowania C ++, pozycja 40.

stefan.gal
źródło
3
To pozwala fun(Foo("text"));.
Guilherme Bernal