¿Por qué puedo definir estructuras y clases dentro de una función en C ++?

90

Simplemente hice algo como esto en C ++ por error y funciona. ¿Por qué puedo hacer esto?

int main(int argc, char** argv) {
    struct MyStruct
    {
      int somevalue;
    };

    MyStruct s;
    s.somevalue = 5;
}

Ahora, después de hacer esto, recordé haber leído sobre este truco en algún lugar, hace mucho tiempo, como una especie de herramienta de programación funcional de los pobres para C ++, pero no puedo recordar por qué esto es válido, o dónde lo leí.

¡Las respuestas a cualquiera de las preguntas son bienvenidas!

Nota: Aunque al escribir la pregunta no obtuve ninguna referencia a esta pregunta , la barra lateral actual lo señala, así que lo pondré aquí como referencia, de cualquier manera la pregunta es diferente pero podría ser útil.

Robert Gould
fuente

Respuestas:

70

[EDITAR 18/4/2013]: Felizmente, la restricción mencionada a continuación se ha eliminado en C ++ 11, ¡así que las clases definidas localmente son útiles después de todo! Gracias al comentarista bamboon.

La capacidad de definir clases localmente haría que la creación de functores personalizados (clases con operator()(), por ejemplo, funciones de comparación para pasar std::sort()o "cuerpos de bucle" para usar std::for_each()) sea mucho más conveniente.

Desafortunadamente, C ++ prohíbe el uso de clases definidas localmente con plantillas , ya que no tienen vínculos. Dado que la mayoría de las aplicaciones de functores involucran tipos de plantilla que se basan en el tipo de functor, las clases definidas localmente no se pueden usar para esto; debe definirlas fuera de la función. :(

[EDITAR 11/1/2009]

La cotización relevante de la norma es:

14.3.1 / 2: .Un tipo local, un tipo sin vinculación, un tipo sin nombre o un tipo compuesto de cualquiera de estos tipos no se utilizarán como argumento de plantilla para un parámetro de tipo de plantilla.

j_random_hacker
fuente
2
Aunque empíricamente, esto parece funcionar con MSVC ++ 8. (Pero no con g ++.)
j_random_hacker
Estoy usando gcc 4.3.3 y parece funcionar allí: pastebin.com/f65b876b . ¿Tiene una referencia de dónde lo prohíbe la norma? Me parece que se podría instanciar fácilmente en el momento de su uso.
Catskul
@Catskul: 14.3.1 / 2: "Un tipo local, un tipo sin vinculación, un tipo sin nombre o un tipo compuesto de cualquiera de estos tipos no se utilizará como argumento de plantilla para un parámetro de tipo de plantilla". Supongo que el fundamento es que las clases locales requerirían otro montón de información para introducir nombres destrozados, pero no lo sé con certeza. Por supuesto, un compilador en particular puede ofrecer extensiones para evitar esto, como parece que lo hacen MSVC ++ 8 y las versiones recientes de g ++.
j_random_hacker
9
Esta restricción se eliminó en C ++ 11.
Stephan Dollberg
31

Una aplicación de clases de C ++ definidas localmente está en el patrón de diseño de fábrica :


// In some header
class Base
{
public:
    virtual ~Base() {}
    virtual void DoStuff() = 0;
};

Base* CreateBase( const Param& );

// in some .cpp file
Base* CreateBase( const Params& p )
{
    struct Impl: Base
    {
        virtual void DoStuff() { ... }
    };

    ...
    return new Impl;
}

Aunque puede hacer lo mismo con el espacio de nombres anónimo.

Nikolai Fetissov
fuente
¡Interesante! Aunque se aplicarán las restricciones con respecto a las plantillas que menciono, este enfoque garantiza que no se pueden crear instancias de Impl (¡ni siquiera hablar de ellas!) Excepto mediante CreateBase (). Por tanto, esta parece una forma excelente de reducir el grado en que los clientes dependen de los detalles de implementación. +1.
j_random_hacker
26
Esa es una buena idea, no estoy seguro de si la usaré pronto, pero probablemente sea buena para sacar en el bar para impresionar a algunas chicas :)
Robert Gould
2
(Estoy hablando de las chicas por cierto, ¡no de la respuesta!)
markh44
9
lol Robert ... Sí, nada impresiona a una mujer como conocer los rincones oscuros de C ++ ...
j_random_hacker
10

En realidad, es muy útil para realizar un trabajo de seguridad de excepciones basado en pilas. O limpieza general de una función con múltiples puntos de retorno. Esto a menudo se denomina el idioma RAII (adquisición de recursos es inicialización).

void function()
{

    struct Cleaner
    {
        Cleaner()
        {
            // do some initialization code in here
            // maybe start some transaction, or acquire a mutex or something
        }

        ~Cleaner()
        {
             // do the associated cleanup
             // (commit your transaction, release your mutex, etc.)
        }
    };

    Cleaner cleaner;

    // Now do something really dangerous
    // But you know that even in the case of an uncaught exception, 
    // ~Cleaner will be called.

    // Or alternatively, write some ill-advised code with multiple return points here.
    // No matter where you return from the function ~Cleaner will be called.
}
simong
fuente
5
Cleaner cleaner();Creo que esta será una declaración de función en lugar de una definición de objeto.
usuario
2
@user Tienes razón. Para llamar al constructor predeterminado, debe escribir Cleaner cleaner;o Cleaner cleaner{};.
callyater
Las clases dentro de las funciones no tienen nada que ver con RAII y, además, este no es un código C ++ válido y no se compilará.
Mikhail Vasilyev
1
Incluso dentro de funciones, clases como esta son PRECISAMENTE de lo que se trata RAII en C ++.
Christopher Bruns
9

Bueno, básicamente, ¿por qué no? A structen C (que se remonta a los albores de los tiempos) era solo una forma de declarar una estructura de registro. Si quiere uno, ¿por qué no poder declararlo donde declararía una variable simple?

Una vez que haga eso, recuerde que el objetivo de C ++ era ser compatible con C si fuera posible. Así que se quedó.

Charlie martin
fuente
una especie de característica interesante haber sobrevivido, pero como j_random_hacker acaba de señalar, no es tan útil como imaginaba en C ++: /
Robert Gould
Sí, las reglas de alcance también eran extrañas en C. Creo que, ahora que tengo más de 25 años de experiencia con C ++, tal vez esforzarse por parecerse tanto a C como ellos podría haber sido un error. Por otro lado, lenguajes más elegantes como Eiffel no fueron adoptados tan fácilmente.
Charlie Martin
Sí, he migrado una base de código C existente a C ++ (pero no a Eiffel).
ChrisW
3

Sirve para crear matrices de objetos que estén correctamente inicializados.

Tengo una clase C que no tiene un constructor predeterminado. Quiero una matriz de objetos de la clase C.Descubro cómo quiero que esos objetos se inicialicen, luego derivo una clase D de C con un método estático que proporciona el argumento para C en el constructor predeterminado de D:

#include <iostream>
using namespace std;

class C {
public:
  C(int x) : mData(x)  {}
  int method() { return mData; }
  // ...
private:
  int mData;
};

void f() {

  // Here I am in f.  I need an array of 50 C objects starting with C(22)

  class D : public C {
  public:
    D() : C(D::clicker()) {}
  private:
    // I want my C objects to be initialized with consecutive
    // integers, starting at 22.
    static int clicker() { 
      static int current = 22;
      return current++;
    } 
  };

  D array[50] ;

  // Now I will display the object in position 11 to verify it got initialized
  // with the right value.  

  cout << "This should be 33: --> " << array[11].method() << endl;

  cout << "sizodf(C): " << sizeof(C) << endl;
  cout << "sizeof(D): " << sizeof(D) << endl;

  return;

}

int main(int, char **) {
  f();
  return 0;
}

En aras de la simplicidad, este ejemplo utiliza un constructor trivial no predeterminado y un caso en el que los valores se conocen en el momento de la compilación. Es sencillo extender esta técnica a los casos en los que desee que se inicialice una matriz de objetos con valores que solo se conocen en tiempo de ejecución.

Thomas L Holaday
fuente
¡Sin duda una aplicación interesante! Sin embargo, no estoy seguro de que sea prudente o incluso seguro: si necesita tratar esa matriz de D como una matriz de C (por ejemplo, debe pasarla a una función que toma un D*parámetro), esto se romperá silenciosamente si D es en realidad más grande que C . (Creo ...)
j_random_hacker
+ j_hacker_aleatorio, tamaño de (D) == tamaño de (C). Agregué un informe de tamaño de () para usted.
Thomas L Holaday