Cómo no permitir provisionales

107

Para una clase Foo, ¿hay alguna manera de no permitir su construcción sin darle un nombre?

Por ejemplo:

Foo("hi");

¿Y solo permitirlo si le da un nombre, como el siguiente?

Foo my_foo("hi");

La vida útil del primero es solo la declaración y el segundo es el bloque adjunto. En mi caso de uso, Fooestá midiendo el tiempo entre el constructor y el destructor. Como nunca me refiero a la variable local, a menudo me olvido de ponerla y accidentalmente cambio la duración. En su lugar, me gustaría obtener un error de tiempo de compilación.

Martín C. Martín
fuente
8
Esto también podría resultar útil para los protectores de bloqueo mutex.
lucas clemente
1
Bueno, podrías escribir tu propio compilador de C ++ donde esté prohibido, pero estrictamente hablando, entonces no sería C ++. También hay lugares donde los temporales como ese serían útiles, como cuando se devuelve un objeto de una función, por ejemplo (como return std::string("Foo");)
Un tipo programador
2
No, no puedes hacer esto, lo siento
Armen Tsirunyan
2
Dependiendo de su religión, este podría ser un caso en el que las macros podrían ser útiles (al usar ese tipo solo a través de una macro que siempre crea una variable)
PlasmaHH
3
Parece más algo que quisiera que mi herramienta LINT detectara que algo que quisiera prevenir sintácticamente mediante un truco del compilador.
Warren P

Respuestas:

101

Otra solución basada en macros:

#define Foo class Foo

La declaración se Foo("hi");expande a class Foo("hi");, que está mal formada; pero se Foo a("hi")expande a lo class Foo a("hi")que es correcto.

Esto tiene la ventaja de que es compatible tanto con el código fuente como con el código binario existente (correcto). (Esta afirmación no es del todo correcta; consulte el comentario de Johannes Schaub y la discusión subsiguiente a continuación: "¿Cómo puede saber que es compatible con el código fuente existente? Su amigo incluye su encabezado y tiene void f () {int Foo = 0;} que anteriormente se compilaba bien y ahora se compila mal. Además, cada línea que define una función miembro de la clase Foo falla: void class Foo :: bar () {} " )

ecatmur
fuente
51
¿Cómo puede saber que es compatible con el código fuente existente? Su amigo incluye su encabezado y lo tiene void f() { int Foo = 0; }previamente compilado bien y ahora compila mal. Además, cada línea que define una función miembro de la clase Foo falla: void class Foo::bar() {}.
Johannes Schaub - litb
21
¿Cómo puede esto conseguir tantos votos? Solo mire el comentario de @ JohannesSchaub-litb y comprenderá que esta es una solución realmente mala. Porque todas las definiciones de funciones miembro son inválidas después de esto .. -1 de mi lado
Aamir
2
@JustMaximumPower: Espero que haya sido sarcástico porque si no, es nuevamente una mala solución (leer peor). Porque volvemos al punto de partida después de indefinirlo, lo que significa que no obtendrá un error de compilación (que el OP pretendía) en una línea similar, es decir, Foo("Hi")dentro de Foo.cpp ahora
Aamir
1
@Aamir No, hablo en serio. Martin C. Martin intenta utilizarlo para proteger el uso de Foo, no la implementación.
JustMaximumPower
1
Probé en Visual Studio 2012 y encontré que class Foo("hi");está bien para compilar.
Fresky
71

¿Qué tal un pequeño truco?

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

void Foo (float);


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

fuente
1
¡Gran truco! Una nota: Foo a("hi");(sin class) también sería un error.
máscara de bits
No estoy seguro de entender. Foo ("hola") intenta llamar a void Foo (float) y da como resultado un error del enlazador. Pero, ¿por qué se llama la versión flotante en lugar de Foo ctor?
undu
2
undu, hm, ¿qué compilador estás usando? gcc 3.4 se queja de que no hay conversión a float. Intenta llamar a una función Fooporque tiene prioridad sobre una clase.
@aleguna en realidad no traté de ejecutar este código, eso fue solo una (mala) suposición: s Pero respondiste mi pregunta de todos modos, no sabía que la función tiene prioridad sobre la clase.
undu
1
@didierc no, no Foo::Foo("hi")está permitido en C ++.
Johannes Schaub - litb
44

Haz que el constructor sea privado pero dale a la clase un método de creación .

dchhetri
fuente
9
-1: ¿Cómo resuelve esto el problema del OP? Todavía puede escribir Foo::create();sobreFoo const & x = Foo::create();
Thomas Eding
@ThomasEding Supongo que tienes razón, no soluciona el problema central de OP, solo lo obliga a pensar y no cometer el error que está cometiendo.
dchhetri
1
@ThomasEding, no puedes protegerte contra usuarios enojados que quieren romper el sistema. Incluso con el truco de @ecatmur puedes decir std::common_type<Foo>::type()y obtienes un archivo temporal. O incluso typedef Foo bar; bar().
Johannes Schaub - litb
@ JohannesSchaub-litb: Pero la gran diferencia es si fue por error o no. Casi no hay forma de escribir std::common_type<Foo>::type()por error. Dejar fuera el Foo const & x = ...por accidente es totalmente creíble.
Thomas Eding
24

Este no da como resultado un error del compilador, sino un error de tiempo de ejecución. En lugar de medir un tiempo incorrecto, obtiene una excepción que también puede ser aceptable.

Cualquier constructor que desee proteger necesita un argumento predeterminado en el que set(guard)se llama.

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

Las caracteristicas son:

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

El caso de f2, f3y el regreso de "hello"no estar querido. Para evitar que se arroje, puede permitir que la fuente de una copia sea temporal, restableciendo el guardpara protegernos ahora en lugar de la fuente de la copia. Ahora también verá por qué usamos los indicadores anteriores: nos permite ser 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;
}; 

Las características para f2, f3y por return "hello"ahora son siempre // OK.

Johannes Schaub - litb
fuente
2
Foo f = "hello"; // may throwEsto es suficiente para asustarme para que nunca use este código.
Thomas Eding
4
@thomas, recomiendo marcar el constructor explicity luego dicho código no se compila más. el objetivo era prohibir lo temporal, y lo hace. si tiene miedo, puede hacer que no se lance estableciendo la fuente de una copia en el constructor de copia o movimiento como no temporal. entonces solo el objeto final de varias copias puede arrojar si aún termina como temporal.
Johannes Schaub - litb
2
Dios mío. No soy un novato en C ++ y C ++ 11, pero no puedo entender cómo funciona esto. ¿Podría agregar un par de explicaciones? ..
Mikhail
6
@Mikhail el orden de destrucción de los objetos temporales que se destruyen en los mismos puntos es el orden inverso a su construcción. El argumento predeterminado que pasa la persona que llama es temporal. Si el Fooobjeto también es temporal, y su duración termina en la misma expresión que el argumento predeterminado, entonces el Foodtor del objeto se invocará antes que el dtor del argumento predeterminado, porque el primero se creó después del segundo.
Johannes Schaub - litb
1
@ JohannesSchaub-litb Muy buen truco. Realmente pensé que es imposible distinguir Foo(...);y Foo foo(...);desde dentro del Foo.
Mikhail
18

Hace unos años escribí un parche para el compilador GNU C ++ que agrega una nueva opción de advertencia para esa situación. Esto se rastrea en un artículo de Bugzilla .

Desafortunadamente, GCC Bugzilla es un cementerio donde las sugerencias de características bien consideradas incluidas en el parche van a morir. :)

Esto fue motivado por el deseo de detectar exactamente el tipo de errores que son el tema de esta pregunta en un código que usa objetos locales como dispositivos para bloquear y desbloquear, medir el tiempo de ejecución, etc.

Kaz
fuente
9

Tal como está, con su implementación, no puede hacer esto, pero puede usar esta regla a su favor:

Los objetos temporales no se pueden vincular a referencias no constantes

Puede mover el código de la clase a una función independiente que toma un parámetro de referencia no constante. Si lo hace, obtendrá un error del compilador si un temporal intenta vincularse a la referencia no constante.

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

Salida

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
fuente
1
@didierc: Siempre que proporcionen una función adicional. Depende de usted no hacerlo. Estamos tratando de modificar una forma de lograr algo no permitido explícitamente por el estándar, por lo que, por supuesto, habrá restricciones.
Alok Save
@didierc el parámetro xes un objeto con nombre, por lo que no está claro si realmente queremos prohibirlo. Si el constructor que habría utilizado es explícito, muchas personas lo harían instintivamente Foo f = Foo("hello");. Creo que se enojarían si fallara. Mi solución inicialmente lo rechazó (y casos muy similares) con una excepción / afirmación-falla y alguien se quejó.
Johannes Schaub - litb
@ JohannesSchaub-litb Sí, OP quiere prohibir descartar el valor generado por un constructor al forzar enlaces. Mi ejemplo está mal.
didierc
7

Simplemente no tiene un constructor predeterminado y requiere una referencia a una instancia en cada constructor.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

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

int main()
{
    S a( selfRef, a );
}
Saludos y hth. - Alf
fuente
3
Buena idea, pero tan pronto como usted tiene una variable: S(selfRef, a);. : /
Xeo
3
@Xeo S(SelfRef, S const& s) { assert(&s == this); }, si se acepta un error en tiempo de ejecución.
6

No, me temo que esto no es posible. Pero podría obtener el mismo efecto creando una macro.

#define FOO(x) Foo _foo(x)

Con esto en su lugar, puede escribir FOO (x) en lugar de Foo my_foo (x).

amaurea
fuente
5
Iba a votar a favor, pero luego vi "puedes crear una macro".
Griwes
1
Ok, se corrigieron los guiones bajos. @Griwes: no seas fundamentalista. Es mejor decir "usar una macro" que "esto no se puede hacer".
amaurea
5
Bueno, no se puede hacer. No ha resuelto el problema en absoluto, todavía es perfectamente legal hacerlo Foo();.
Puppy
11
Ahora estás siendo terco aquí. Cambie el nombre de la clase Foo a algo complicado y llame a la macro Foo. Problema resuelto.
amaurea
8
Algo como:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley
4

Dado que el objetivo principal es prevenir errores, considere esto:

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 esa manera no puede olvidarse de nombrar la variable y no puede olvidarse de escribir struct. Detallado, pero seguro.

Daniel Frey
fuente
1

Declare un constructor paramétrico como explícito y nadie jamás creará un objeto de esa clase sin querer.

Por ejemplo

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

void fun(const Foo&);

solo se puede usar de esta manera

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

pero nunca de esta manera (a través de un temporal en la pila)

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

Consulte también: Alexandrescu, Estándares de codificación C ++, artículo 40.

stefan.gal
fuente
3
Esto permite fun(Foo("text"));.
Guilherme Bernal