Como proibir temporários

107

Para uma classe Foo, existe uma maneira de proibir sua construção sem dar a ela um nome?

Por exemplo:

Foo("hi");

E só permite se você der um nome, como o seguinte?

Foo my_foo("hi");

O tempo de vida do primeiro é apenas a instrução e o segundo é o bloco envolvente. No meu caso de uso, Fooé medir o tempo entre o construtor e o destruidor. Como nunca me refiro à variável local, frequentemente esqueço de inseri-la e, acidentalmente, mudo o tempo de vida. Eu gostaria de obter um erro de tempo de compilação.

Martin C. Martin
fonte
8
Isso também pode ser útil para protetores de bloqueio mutex.
lucas clemente
1
Bem, você poderia escrever seu próprio compilador C ++ onde fosse proibido, mas estritamente falando, então não seria C ++. Também há lugares onde temporários como esse seriam úteis, como ao retornar um objeto de uma função por exemplo (como return std::string("Foo");)
Algum programador cara
2
Não, você não pode fazer isso, desculpe
Armen Tsirunyan
2
Dependendo da sua religião, este pode ser um caso em que macros podem ser úteis (usando esse tipo apenas por meio de uma macro que sempre cria uma variável)
PlasmaHH
3
Parece mais algo que eu gostaria que minha ferramenta LINT detectasse do que algo que eu gostaria de impedir sintaticamente por um hack do compilador.
Warren P

Respostas:

101

Outra solução baseada em macro:

#define Foo class Foo

A declaração se Foo("hi");expande para class Foo("hi");, que está malformada; mas se Foo a("hi")expande para class Foo a("hi"), o que é correto.

Isso tem a vantagem de ser compatível com o código-fonte e o binário com o código existente (correto). (Esta afirmação não está totalmente correta - consulte o comentário de Johannes Schaub e a discussão subsequente abaixo: "Como você pode saber se o código-fonte é compatível com o código existente? Seu amigo inclui seu cabeçalho e tem void f () {int Foo = 0;} que anteriormente compilava bem e agora compila incorretamente! Além disso, cada linha que define uma função de membro da classe Foo falha: void class Foo :: bar () {} " )

ecatmur
fonte
51
Como você pode saber se é fonte compatível com o código existente? Seu amigo inclui seu cabeçalho e void f() { int Foo = 0; }que compilou corretamente e agora compila incorretamente! Além disso, cada linha que define uma função membro da classe Foo falha: void class Foo::bar() {}.
Johannes Schaub - litb
21
Como isso pode conseguir tantos votos? Basta olhar para o comentário de @ JohannesSchaub-litb e você entenderá que essa é uma solução realmente ruim. Porque todas as definições de funções-membro são inválidas depois disso .. -1 do meu lado
Aamir
2
@JustMaximumPower: Espero que tenha sido sarcástico, porque senão, é novamente uma má (leia pior) solução alternativa. Porque estamos de volta à estaca zero após indefini-la, o que significa que você não obterá um erro de compilação (que o OP pretendia) em uma linha semelhante, ou seja, Foo("Hi")dentro de Foo.cpp agora
Aamir
1
@Aamir Não, estou falando sério. Martin C. Martin pretende usá-lo para proteger o uso de Foo, não a implementação.
JustMaximumPower
1
Tentei no Visual Studio 2012 e encontrei class Foo("hi");OK para compilar.
fresky
71

Que tal um pequeno hack

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

void Foo (float);


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

fonte
1
Grande hack! Uma nota: Foo a("hi");(sem class) seria um erro também.
bitmask
Eu não tenho certeza se entendi. Foo ("hello") tenta chamar void Foo (float) e resulta em um erro de linker? Mas por que a versão float é chamada em vez do Foo ctor?
undu
2
undu, hm qual compilador você está usando? O gcc 3.4 reclama que não há conversão para float. Ele tenta chamar uma função Fooporque tem precedência sobre uma classe.
@aleguna, na verdade, não tentei executar este código, foi apenas um palpite (ruim): s Mas você respondeu à minha pergunta de qualquer maneira, eu não sabia que a função tem precedência sobre a classe.
undu
1
@didierc não, não Foo::Foo("hi")é permitido em C ++.
Johannes Schaub - litb
44

Torne o construtor privado, mas dê à classe um método de criação .

dchhetri
fonte
9
-1: Como isso resolve o problema do OP? Você ainda pode escrever Foo::create();sobreFoo const & x = Foo::create();
Thomas Eding
@ThomasEding Acho que você está certo, isso não resolve o problema central do OP, mas apenas o força a pensar e não cometer o erro que está cometendo.
dchhetri
1
@ThomasEding você não pode se proteger contra usuários irritados que querem quebrar o sistema. Mesmo com o hack de @ecatmur, você pode dizer std::common_type<Foo>::type()e obter um temporário. Ou mesmo typedef Foo bar; bar().
Johannes Schaub - litb
@ JohannesSchaub-litb: Mas a grande diferença é se foi ou não por engano ou não. Quase não há como digitar std::common_type<Foo>::type()por engano. Deixar de fora o Foo const & x = ...acidente é totalmente verossímil.
Thomas Eding
24

Este não resulta em um erro do compilador, mas um erro de tempo de execução. Em vez de medir um tempo errado, você obtém uma exceção que também pode ser aceitável.

Qualquer construtor que você deseja proteger precisa de um argumento padrão no qual set(guard)é chamado.

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

As características são:

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

O caso de f2, f3eo retorno de "hello"não ser querido. Para evitar o lançamento, você pode permitir que a origem de uma cópia seja temporária, redefinindo o guardpara agora nos proteger em vez da origem da cópia. Agora você também vê porque usamos as dicas acima - isso nos permite ser flexíveis.

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

As características para f2, f3e para return "hello"agora são sempre // OK.

Johannes Schaub - litb
fonte
2
Foo f = "hello"; // may throwIsso é o suficiente para me assustar e nunca mais usar esse código.
Thomas Eding
4
@thomas, eu recomendo marcar o construtor explicite então esse código não compila mais. o objetivo era proibir o temporário, e é o que acontece. se estiver com medo, você pode fazer com que ele não seja jogado definindo a origem de uma cópia no construtor de cópia ou movimento como não temporário. então, apenas o objeto final de várias cópias pode ser lançado, se ainda assim terminar como temporário.
Johannes Schaub - litb
2
Meu Deus. Não sou novato em C ++ e C ++ 11, mas não consigo entender como isso funciona. Você poderia adicionar algumas explicações? ..
Mikhail
6
@Mikhail a ordem de destruição de objetos temporários que são destruídos nos mesmos pontos é a ordem inversa de sua construção. O argumento padrão que o chamador passa é temporário. Se o Fooobjeto também for temporário e seu tempo de vida terminar na mesma expressão do argumento padrão, o Foodtor do objeto será invocado antes do dtor do argumento padrão, porque o primeiro foi criado depois do último.
Johannes Schaub - litb
1
@ JohannesSchaub-litb Muito bom truque. Eu realmente pensei que é impossível distinguir Foo(...);e Foo foo(...);de dentro do Foo.
Mikhail
18

Alguns anos atrás, escrevi um patch para o compilador GNU C ++ que adiciona uma nova opção de aviso para essa situação. Isso é rastreado em um item do Bugzilla .

Infelizmente, o GCC Bugzilla é um cemitério onde sugestões de recursos bem consideradas com patch incluído vão morrer. :)

Isso foi motivado pelo desejo de detectar exatamente o tipo de bugs que são o assunto desta questão no código que usa objetos locais como dispositivos para bloquear e desbloquear, medir o tempo de execução e assim por diante.

Kaz
fonte
9

Como está, com sua implementação, você não pode fazer isso, mas pode usar esta regra a seu favor:

Objetos temporários não podem ser vinculados a referências não constantes

Você pode mover o código da classe para uma função independente que usa um parâmetro de referência não const. Se você fizer isso, obterá um erro do compilador se um temporário tentar se vincular à referência não const.

Amostra de Código

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

Resultado

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
fonte
1
@didierc: Desde que forneçam uma função adicional. Depende de você não fazer isso. Estamos tentando ajustar uma maneira de alcançar algo não explicitamente permitido pelo padrão, então é claro que haverá restrições.
Alok Save
@didierc o parâmetro xé um objeto nomeado, então não está claro se realmente queremos proibi-lo. Se o construtor que você usaria for explícito, as pessoas o fariam instintivamente Foo f = Foo("hello");. Acho que eles ficariam com raiva se falhasse. Minha solução inicialmente rejeitou (e casos muito semelhantes) com uma exceção / falha de afirmação e alguém reclamou.
Johannes Schaub - litb
@ JohannesSchaub-litb Sim, o OP quer proibir o descarte do valor gerado por um construtor forçando ligações. Meu exemplo está errado.
didierc
7

Simplesmente não tem um construtor padrão e exige uma referência a uma instância em cada construtor.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

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

int main()
{
    S a( selfRef, a );
}
Saúde e hth. - Alf
fonte
3
Boa idéia, mas assim que você tem uma variável: S(selfRef, a);. : /
Xeo
3
@Xeo S(SelfRef, S const& s) { assert(&s == this); }, se um erro de tempo de execução for aceitável.
6

Não, infelizmente isso não é possível. Mas você pode obter o mesmo efeito criando uma macro.

#define FOO(x) Foo _foo(x)

Com isso no lugar, você pode simplesmente escrever FOO (x) em vez de Foo my_foo (x).

amaurea
fonte
5
Eu ia votar positivamente, mas então vi "você poderia criar uma macro".
Griwes
1
Ok, corrigiu os sublinhados. @Griwes - Não seja um fundamentalista. É melhor dizer "usar uma macro" do que "isso não pode ser feito".
amaurea
5
Bem, isso não pode ser feito. Você ainda não resolveu o problema, ainda é perfeitamente legal fazê-lo Foo();.
Cachorro
11
Agora você está sendo teimoso aqui. Renomeie a classe Foo para algo complicado e chame a macro de Foo. Problema resolvido.
amaurea
8
Algo como:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley
4

Como o objetivo principal é evitar bugs, considere o seguinte:

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
}

Dessa forma, você não pode esquecer de nomear a variável e não pode se esquecer de escrever struct. Detalhado, mas seguro.

Daniel Frey
fonte
1

Declare um construtor paramétrico como explícito e ninguém jamais criará um objeto dessa classe acidentalmente.

Por exemplo

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

void fun(const Foo&);

só pode ser usado desta forma

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

mas nunca desta forma (através de um temporário na pilha)

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

Veja também: Alexandrescu, C ++ Coding Standards, Item 40.

stefan.gal
fonte
3
Isso permite fun(Foo("text"));.
Guilherme Bernal