Pourquoi puis-je définir des structures et des classes dans une fonction en C ++?

90

J'ai juste fait par erreur quelque chose comme ça en C ++, et ça marche. Pourquoi puis-je faire ça?

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

    MyStruct s;
    s.somevalue = 5;
}

Maintenant, après avoir fait cela, je me suis souvenu avoir lu quelque part à propos de cette astuce, il y a longtemps, comme une sorte d'outil de programmation fonctionnel pour homme pauvre pour C ++, mais je ne me souviens pas pourquoi cela est valide, ni où je l'ai lu.

Les réponses à l'une ou l'autre des questions sont les bienvenues!

Remarque: Bien que lors de la rédaction de la question, je n'ai pas obtenu de références à cette question , la barre latérale actuelle le souligne donc je la mets ici pour référence, de toute façon la question est différente mais pourrait être utile.

Robert Gould
la source

Réponses:

70

[EDIT 18/4/2013]: Heureusement, la restriction mentionnée ci-dessous a été levée en C ++ 11, donc les classes définies localement sont utiles après tout! Merci au commentateur Bamboon.

La capacité de définir des classes au niveau local serait rendre la création foncteurs personnalisés (classes avec un operator()(), par exemple des fonctions de comparaison pour passer à std::sort()ou « corps de boucle » à utiliser avec std::for_each()) beaucoup plus pratique.

Malheureusement, C ++ interdit l'utilisation de classes définies localement avec des modèles , car elles n'ont pas de lien. Étant donné que la plupart des applications de foncteurs impliquent des types de modèles qui sont basés sur le type de foncteur, les classes définies localement ne peuvent pas être utilisées pour cela - vous devez les définir en dehors de la fonction. :(

[EDIT 1/11/2009]

La citation pertinente de la norme est:

14.3.1 / 2: .Un type local, un type sans liaison, un type sans nom ou un type composé de l'un de ces types ne doit pas être utilisé comme argument modèle pour un paramètre de type modèle.

j_random_hacker
la source
2
Bien qu'empiriquement, cela semble fonctionner avec MSVC ++ 8. (Mais pas avec g ++.)
j_random_hacker
J'utilise gcc 4.3.3, et il semble y fonctionner: pastebin.com/f65b876b . Avez-vous une référence à l'endroit où la norme l'interdit? Il me semble qu'il pourrait facilement être instancié au moment de l'utilisation.
Catskul
@Catskul: 14.3.1 / 2: "Un type local, un type sans lien, un type sans nom ou un type composé de l'un de ces types ne doit pas être utilisé comme argument-modèle pour un paramètre de type de modèle". Je suppose que la raison est que les classes locales exigeraient encore un autre ensemble d'informations pour être poussées dans des noms mutilés, mais je ne le sais pas avec certitude. Bien sûr, un compilateur particulier peut proposer des extensions pour contourner ce problème, comme il semble que MSVC ++ 8 et les versions récentes de g ++ le font.
j_random_hacker
9
Cette restriction a été levée dans C ++ 11.
Stephan Dollberg
31

Une application des classes C ++ définies localement est dans le modèle de conception Factory :


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

Bien que vous puissiez faire la même chose avec un espace de noms anonyme.

Nikolai Fetissov
la source
Intéressant! Bien que les restrictions concernant les modèles que je mentionne s'appliquent, cette approche garantit que les instances d'Impl ne peuvent pas être créées (ou même discutées!) Sauf par CreateBase (). Cela semble donc être un excellent moyen de réduire la mesure dans laquelle les clients dépendent des détails de la mise en œuvre. +1.
j_random_hacker
26
C'est une bonne idée, je ne sais pas si je vais l'utiliser de sitôt, mais probablement une bonne idée à retirer au bar pour impressionner des filles :)
Robert Gould
2
(Je parle des poussins BTW, pas de la réponse!)
markh44
9
lol Robert ... Ouais, rien n'impressionne vraiment une femme comme connaître les recoins obscurs de C ++ ...
j_random_hacker
10

C'est en fait très utile pour effectuer des travaux de sécurité des exceptions basés sur la pile. Ou nettoyage général d'une fonction avec plusieurs points de retour. C'est ce qu'on appelle souvent l'idiome RAII (l'acquisition de ressources est l'initialisation).

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
la source
5
Cleaner cleaner();Je pense que ce sera une déclaration de fonction plutôt qu'une définition d'objet.
utilisateur
2
@user Vous avez raison. Pour appeler le constructeur par défaut, il doit écrire Cleaner cleaner;ou Cleaner cleaner{};.
callyalater
Les classes à l'intérieur des fonctions n'ont rien à voir avec RAII et, de plus, ce n'est pas un code C ++ valide et ne se compilera pas.
Mikhail Vasilyev
1
Même à l'intérieur des fonctions, des classes comme celle-ci sont PRÉCISÉMENT ce qu'est RAII en C ++.
Christopher Bruns
9

Eh bien, au fond, pourquoi pas? A structen C (remontant à la nuit des temps) n'était qu'un moyen de déclarer une structure d'enregistrement. Si vous en voulez un, pourquoi ne pas pouvoir le déclarer là où vous déclareriez une simple variable?

Une fois que vous avez fait cela, rappelez-vous que l'un des objectifs de C ++ était d'être compatible avec C si possible. Alors ça est resté.

Charlie Martin
la source
une sorte de fonctionnalité intéressante qui a survécu, mais comme j_random_hacker vient de le souligner, ce n'est pas aussi utile que je l'imaginais en C ++: /
Robert Gould
Ouais, les règles de cadrage étaient bizarres en C aussi. Je pense, maintenant que j'ai plus de 25 ans d'expérience avec C ++, que peut-être s'efforcer d'être autant comme C qu'eux aurait pu être une erreur. D'un autre côté, des langues plus élégantes comme Eiffel n'ont pas été adoptées aussi facilement.
Charlie Martin
Oui, j'ai migré une base de code C existante vers C ++ (mais pas vers Eiffel).
ChrisW
3

C'est pour créer des tableaux d'objets correctement initialisés.

J'ai une classe C qui n'a pas de constructeur par défaut. Je veux un tableau d'objets de la classe C.Je comprends comment je veux que ces objets soient initialisés, puis dérive une classe D de C avec une méthode statique qui fournit l'argument pour le C dans le constructeur par défaut 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;
}

Par souci de simplicité, cet exemple utilise un constructeur trivial non par défaut et un cas où les valeurs sont connues au moment de la compilation. Il est simple d'étendre cette technique aux cas où vous souhaitez qu'un tableau d'objets soit initialisé avec des valeurs connues uniquement à l'exécution.

Thomas L Holaday
la source
Certainement une application intéressante! Pas sûr que ce soit sage ou même sûr - si vous devez traiter ce tableau de D comme un tableau de C (par exemple, vous devez le passer à une fonction prenant un D*paramètre), cela se cassera silencieusement si D est en fait plus grand que C . (Je pense ...)
j_random_hacker
+ j_random_hacker, sizeof (D) == sizeof (C). J'ai ajouté un rapport sizeof () pour vous.
Thomas L Holaday