Warum kann ich Strukturen und Klassen innerhalb einer Funktion in C ++ definieren?

87

Ich habe so etwas fälschlicherweise in C ++ gemacht, und es funktioniert. Warum kann ich das tun?

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

    MyStruct s;
    s.somevalue = 5;
}

Nachdem ich das getan hatte, erinnerte ich mich daran, dass ich vor langer Zeit irgendwo über diesen Trick gelesen hatte, als eine Art funktionales Programmierwerkzeug für C ++, aber ich kann mich nicht erinnern, warum dies gültig ist oder wo ich es gelesen habe.

Antworten auf beide Fragen sind willkommen!

Hinweis: Obwohl ich beim Schreiben der Frage keine Verweise auf diese Frage erhalten habe, weist die aktuelle Seitenleiste darauf hin, sodass ich sie hier als Referenz einfügen werde. In beiden Fällen ist die Frage anders, könnte aber nützlich sein.

Robert Gould
quelle

Antworten:

69

[EDIT 18/4/2013]: Glücklicherweise wurde die unten erwähnte Einschränkung in C ++ 11 aufgehoben, sodass lokal definierte Klassen schließlich nützlich sind! Vielen Dank an Kommentator Bambus.

Die Möglichkeit, Klassen lokal zu definieren, würde das Erstellen benutzerdefinierter Funktoren (Klassen mit operator()()z. B. Vergleichsfunktionen zum Übergeben an std::sort()oder "Schleifenkörper", mit denen verwendet werden soll std::for_each()) wesentlich komfortabler machen.

Leider verbietet C ++ die Verwendung lokal definierter Klassen mit Vorlagen , da diese keine Verknüpfung haben. Da die meisten Anwendungen von Funktoren Vorlagentypen umfassen, die als Vorlage für den Funktortyp dienen, können lokal definierte Klassen hierfür nicht verwendet werden. Sie müssen sie außerhalb der Funktion definieren. :(

[EDIT 1/11/2009]

Das relevante Zitat aus dem Standard lautet:

14.3.1 / 2: Ein lokaler Typ, ein Typ ohne Verknüpfung, ein unbenannter Typ oder ein Typ, der aus einem dieser Typen zusammengesetzt ist, darf nicht als Vorlagenargument für einen Vorlagentypparameter verwendet werden.

j_random_hacker
quelle
2
Obwohl empirisch, scheint dies mit MSVC ++ 8 zu funktionieren. (Aber nicht mit g ++.)
j_random_hacker
Ich benutze gcc 4.3.3 und es scheint dort zu funktionieren: pastebin.com/f65b876b . Haben Sie einen Hinweis darauf, wo der Standard dies verbietet? Es scheint mir, dass es zum Zeitpunkt der Verwendung leicht instanziiert werden könnte.
Catskul
@Catskul: 14.3.1 / 2: "Ein lokaler Typ, ein Typ ohne Verknüpfung, ein unbenannter Typ oder ein Typ, der aus einem dieser Typen zusammengesetzt ist, darf nicht als Vorlagenargument für einen Vorlagentypparameter verwendet werden." Ich denke, das Grundprinzip ist, dass lokale Klassen noch eine Reihe von Informationen benötigen würden, um in verstümmelte Namen umgewandelt zu werden, aber das weiß ich nicht genau. Natürlich kann ein bestimmter Compiler Erweiterungen anbieten, um dies zu umgehen, wie es MSVC ++ 8 und neuere Versionen von g ++ scheinen.
j_random_hacker
8
Diese Einschränkung wurde in C ++ 11 aufgehoben.
Stephan Dollberg
31

Eine Anwendung lokal definierter C ++ - Klassen befindet sich im Factory-Entwurfsmuster :


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

Sie können dies jedoch auch mit einem anonymen Namespace tun.

Nikolai Fetissov
quelle
Interessant! Obwohl die von mir erwähnten Einschränkungen in Bezug auf Vorlagen gelten, garantiert dieser Ansatz, dass Instanzen von Impl nur von CreateBase () erstellt (oder sogar besprochen!) Werden können. Dies scheint also eine hervorragende Möglichkeit zu sein, um das Ausmaß zu verringern, in dem Kunden von Implementierungsdetails abhängig sind. +1.
j_random_hacker
25
Das ist eine nette Idee, nicht sicher, ob ich sie bald verwenden werde, aber wahrscheinlich eine gute, um sie an der Bar herauszuziehen und ein paar Küken zu beeindrucken :)
Robert Gould
2
(Ich spreche
übrigens
9
lol Robert ... Ja, nichts beeindruckt eine Frau so sehr wie das Wissen über dunkle Ecken von C ++ ...
j_random_hacker
10

Es ist tatsächlich sehr nützlich, um einige stapelbasierte Ausnahmesicherheitsarbeiten durchzuführen. Oder allgemeine Bereinigung von einer Funktion mit mehreren Rückgabepunkten. Dies wird häufig als RAII-Idiom (Resource Acquisition is Initialzation) bezeichnet.

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
quelle
5
Cleaner cleaner();Ich denke, dies wird eher eine Funktionsdeklaration als eine Objektdefinition sein.
Benutzer
2
@user Du bist richtig. Um den Standardkonstruktor aufzurufen, sollte er Cleaner cleaner;oder schreiben Cleaner cleaner{};.
Callyalater
Klassen in Funktionen haben nichts mit RAII zu tun. Außerdem ist dies kein gültiger C ++ - Code und wird nicht kompiliert.
Mikhail Vasilyev
1
Selbst innerhalb von Funktionen sind Klassen wie diese genau das, worum es bei RAII in C ++ geht.
Christopher Bruns
9

Warum eigentlich nicht? A structin C (bis in die Anfänge der Zeit) war nur eine Möglichkeit, eine Datensatzstruktur zu deklarieren. Wenn Sie eine möchten, können Sie sie dort deklarieren, wo Sie eine einfache Variable deklarieren würden.

Wenn Sie dies getan haben, denken Sie daran, dass ein Ziel von C ++ darin bestand, wenn möglich mit C kompatibel zu sein. So blieb es.

Charlie Martin
quelle
Eine nette Sache, um überlebt zu haben, aber wie j_random_hacker gerade betonte, ist es nicht so nützlich, wie ich es mir in C ++ vorgestellt habe: /
Robert Gould
Ja, die Scoping-Regeln waren auch in C seltsam. Ich denke, jetzt, wo ich mehr als 25 Jahre Erfahrung mit C ++ habe, könnte es ein Fehler gewesen sein, C so ähnlich zu sein wie sie. Auf der anderen Seite wurden elegantere Sprachen wie Eiffel bei weitem nicht so schnell übernommen.
Charlie Martin
Ja, ich habe eine vorhandene C-Codebasis nach C ++ migriert (aber nicht nach Eiffel).
ChrisW
3

Es dient zum Erstellen von Arrays von Objekten, die ordnungsgemäß initialisiert wurden.

Ich habe eine Klasse C, die keinen Standardkonstruktor hat. Ich möchte ein Array von Objekten der Klasse C. Ich finde heraus, wie diese Objekte initialisiert werden sollen, und leite dann eine Klasse D von C mit einer statischen Methode ab, die das Argument für das C im Standardkonstruktor von D bereitstellt:

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

In diesem Beispiel wird der Einfachheit halber ein trivialer Nicht-Standard-Konstruktor und ein Fall verwendet, in dem die Werte zur Kompilierungszeit bekannt sind. Es ist einfach, diese Technik auf Fälle zu erweitern, in denen ein Array von Objekten mit Werten initialisiert werden soll, die nur zur Laufzeit bekannt sind.

Thomas L Holaday
quelle
Mit Sicherheit eine interessante Anwendung! Ich bin mir nicht sicher, ob es sinnvoll oder sogar sicher ist. Wenn Sie dieses Array von D als Array von C behandeln müssen (z. B. müssen Sie es an eine Funktion übergeben, die einen D*Parameter verwendet), wird dies stillschweigend unterbrochen, wenn D tatsächlich größer als C ist (Ich denke ...)
j_random_hacker
+ j_random_hacker, sizeof (D) == sizeof (C). Ich habe einen sizeof () - Bericht für Sie hinzugefügt.
Thomas L Holaday