26.04.2013
Version: 1
Inhaltsverzeichnis
1. Unterscheide Funktions-Deklarationen und Objekt-Erzeugungen
Ist das wirklich einen Artikel wert - die Unterscheidung von Funktions-Deklarationen und Objekt-Erzeugungen? Das sind doch zwei sehr verschiedene Dinge, die schon ein Anfänger nicht durcheinander bringt, oder?Wer so denkt, hat die Rechnung ohne den Wirt gemacht - hier in Form einer manchmal kruden C++ Syntax. Es gibt leider Situationen, die nicht so offensichtlich von vielen Programmieren erkannt werden.
1.1 Funktions-Deklaration statt Objekt-Erzeugung
Ein typische Fehler, den viele C++ Programmierer kennen, da er heute in jedem besseren C++ Buch oder Tutorial[1]. beschrieben ist, ist die fehlerhafte Initialisierung eines lokalen Objekts mit denn leeren runden Klammern - siehe folgendes Beispiel für das Symbol "a2".
class A
{
public:
A();
A(int);
};
int main()
{
A a1; // okay
A a2(); // Syntaktisch auch okay, aber was ist das hier?
A a3(5); // okay
}
Typ Name Klammer-auf Klammer-zu
Die beiden anderen Zeilen mit "a1" und "a3" sind dagegen problemlos:
- Bei "a1" besteht die Anweisung nur aus "Typ" und "Name", und das ist eine Objekt-Erzeugung. Erst die runden Klammern könnten aus diesem Anfang eine Funktions-Deklaration machen.
- Die Konstruktion von "a3" bringt zwar runde Klammern mit, aber hier findet sich in den Klammern kein Typ, sondern ein Wert. Dieser feine Unterschied ist hier entscheidend.
class A
{
public:
A();
A(int);
};
int main()
{
int var = 2;
A a1(1); // Objekt-Erzeugung, da "1" ein Wert ist
A a2(var); // Objekt-Erzeugung, da "var" ein Wert ist
A f1(int); // Funktions-Deklaration, da "int" ein Typ ist
A f2(A); // Funktions-Deklaration, da "A" ein Typ ist
}
1.2 Das Problem betrifft nicht nur lokale Objekte
Das Problem ist nicht nur auf lokale Objekte beschränkt, sondern betrifft auch globale oder statische Objekte. Ein Beispiel:
#include <string>
std::string s1; // Globales Objekt vom Typ "std::string"
std::string s2(); // Funktions-Deklaration "s2"
static std::string s3; // Statisches (quelltextlokales) Objekt vom Typ "std::string"
static std::string s4(); // Funktions-Deklaration "s4" einer quelltextlokalen Funktion
class A
{
private:
int n1; // Ein normales "int" Attribute der Klasse "A"
int n2(); // Deklaration der Element-Funktion "n2" ohne Parameter und mit Int-Rückgabe
};
1.3 Compiler-Meldungen
Da die "semantisch fehlerhafte Anweisung" syntaktisch okay ist, bekommt man an der Stelle der Funktions-Deklaration keine Fehler-Meldung vom Compiler. Die bekommt man erst, wenn man das vermeintliche Objekt, das ja in Wirklichkeit eine Funktion ist, nutzen will.
class A
{
public:
A();
void f();
};
int main()
{
A a(); // Hier keine Meldung, warum auch?
a.f(); // Compiler-Fehler erst bei der Nutzung des vermeintlichen Objekts "a"
}
-
Microsoft Visual Studio 6
Line (11) : error C2228: left of '.f' must have class/struct/union type -
Microsoft Visual Studio 2003.NET (deutsch)
Zeile (11): error C2228: Der linke Teil von '.f' muss eine Klasse/Struktur/Union sein
Typ ist 'overloaded-function' -
Microsoft Visual Studio 2010
main.cpp(11): error C2228: left of '.f' must have class/struct/union -
Microsoft Visual Studio 2012
main.cpp(11): error C2228: Links von ".f" muss sich eine Klasse/Struktur/Union befinden. -
Microsoft Visual Studio 2012 mit Nov 2012 CTP
main.cpp(11): error C2228: left of '.f' must have class/struct/union -
GCC 4.7.2 (unter Ubuntu Quetzal)
main.cc:11:6: Fehler: Abfrage des Elementes "f" in "a", das vom Nicht-Klassentyp "A()" ist -
Clang 3.0.6 (unter Ubuntu Quetzal)
main.cc:11:6: error: member reference base type "A (*)()" is not a structure or union -
Comeau Online-Compiler[2] Version 4.3.10.1 Beta
Line 11: error: expression must have class type
Eine mögliche sinnvolle Regel könnte hier sein, daß der Compiler warnt, wenn die Funktions-Deklaration in einem lokalen Scope vorgenommen wurde, und die Funktion nicht genutzt wird. Wir werden später sehen, dass manche Compiler das in manchen Fällen auch so machen.
Diese Regel hilft aber nur weiter, wenn die Funktion nicht genutzt wird. In unserem Fall ist dies aber nicht so, da wir die Funktion ja nutzen, nur eben fehlerhaft als Objekt. Hier helfen nur bessere Fehler-Meldungen für die falsche Funktions-Nutzung weiter. Z.B. Clang geht hier mit der Angabe des korrekten Typ von "a" in die richtige Richtung - wirklich gut ist die Fehler-Meldung aber trotzdem nicht.
2. Aber es gibt noch schlimmere Fallen
Es gibt noch schlimmere Dinge in der C++ Syntax. Sie glauben mir nicht - passen Sie auf.Was ist "xyz" in der folgenden Anweisung?
long xyz(int());
- "int()" ist der Aufruf des Default-Konstruktors von "int", der ein "int" "Objekt" mit dem Wert "0" konstruiert. Also ist "int()" ein Wert.
-
Damit ist der syntaktische Aufbau:
Typ Name Klammer-auf Wert Klammer-zu
- Also ist das eine Objekt-Erzeugung des Objekts "xyz" vom Typ "long" - initialisiert mit dem "int" Wert "0".
In Wirklichkeit ist das die Deklaration einer freien Funktion "xyz", die einen Funktions-Zeiger auf eine Funktion ohne Parameter mit Int-Rückgabe erwartet, und einen "long" zurückgibt. Falls Sie das nicht glauben - folgendes Beispiel zeigt dass es wirklich so ist. Ohne die Deklaration der Funktion "xyz" in der Zeile (*) wäre sie in der nachfolgenden Zeile (**) nicht aufrufbar.
#include <iostream>
using namespace std;
int f()
{
cout << "Funktion f" << endl;
return 0;
}
int main()
{
cout << "Deklaration und Aufruf der Funktion xyz" << endl;
long xyz(int()); // (*)
xyz(f); // (**)
}
long xyz(int(*pf)())
{
cout << "Funktion xyz" << endl;
return pf();
}
Ausgabe:
Deklaration und Aufruf der Funktion xyz
Funktion xyz
Funktion f
#include <string>
#include <vector>
class A
{
public:
A(const char*); // Const-Char-Zeiger-Konstruktor
A(const std::string&); // String-Konstruktor
};
class B
{
public:
B(const std::vector<int>&); // Vektor<Int>-Konstruktor
};
int main()
{
A a(std::string()); // Ich will explizit den String-Konstruktor haben
B b(std::vector<int>()); // Fuer Vektoren gibt es keinerlei Literale
}
Und das gleiche für "b" - auch eine Funktions-Deklaration.
2.1 Wie kommt denn das?
Um das zu verstehen, fangen wir erstmal klein an. Definieren wir doch einfach mal einen Funktions-Zeiger "pf", der auf eine Funktion zeigt, die einen "int" zurückgibt und keinen Parameter erwartet. Bei den meisten Programmierern sieht diese Deklaration so aus:
int(*pf)();
#include <iostream>
using namespace std;
int f()
{
cout << "Funktion f" << endl;
return 0;
}
int main()
{
cout << "Rufe Funktion f ueber Funktions-Zeiger pf auf" << endl;
int(*pf)() = f;
pf();
}
Ausgabe:
Rufe Funktion f ueber Funktions-Zeiger pf auf
Funktion f
void g(int (*pf)()); // Deklaration von "g"
void g(int (*pf)()) // Definition von "g"
{
cout << "Funktion g" << endl;
pf();
}
void g(int (*pf)()); // Original-Deklaration
void g(int (pf)()); // Die gleiche Deklaration nur ohne Zeiger-Syntax
void g(int (*pf)()); // Original-Deklaration
void g(int (pf)()); // Die gleiche Deklaration nur ohne Zeiger-Syntax
void g(int pf()); // Klammern um Parameter-Namen sind optional
void g(int (*pf)()); // Original-Deklaration
void g(int (pf)()); // Die gleiche Deklaration nur ohne Zeiger-Syntax
void g(int pf()); // Klammern um Parameter-Namen sind optional
void g(int()); // Und Parameter-Namen sind auch optional
#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
void g1(int (*pf)());
void g2(int (pf)());
void g3(int pf());
void g4(int());
cout << typeid(g1).name() << endl;
cout << typeid(g2).name() << endl;
cout << typeid(g3).name() << endl;
cout << typeid(g4).name() << endl;
}
Ausgabe: (hier mit dem Microsoft Visual Studio 2012)
void __cdecl(int (__cdecl*)(void))
void __cdecl(int (__cdecl*)(void))
void __cdecl(int (__cdecl*)(void))
void __cdecl(int (__cdecl*)(void))
#include <string>
class A
{
public:
A(const std::string&);
void fct();
};
int main()
{
A a(std::string()); // Keine Objekt-Erzeugung, sondern Funktions-Deklaration
a.fct(); // Compiler-Fehler
}
2.2 Lösungen
Die wohl einfachste Lösung ist, das Konstruktor-Argument als temporäre Variable zu erzeugen und diese dann zu übergeben.
std::string temp;
A a(temp); // Diesmal ist "a" ganz eindeutig ein Objekt, da "temp" ein Wert ist
A a((std::string())); // Man beachte die extra Klammerung des Arguments
Mit den extra Klammern funktioniert alles:
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
A(const std::string&) {}
void fct() { std::cout << "A::fct()" << std::endl; }
};
int main()
{
A a((std::string())); // Mit extra Klammern kein Problem - "a" ist Objekt
a.fct(); // Normaler Aufruf der Element-Funktion "fct"
}
Ausgabe:
A::fct()
2.3 Ältere Compiler
Es soll ältere Compiler geben, die Probleme mit den extra Klammern haben. Für die sollte der folgende Quelltext immer noch nicht gültig sein. Leider kenne ich keinen solchen Compiler. Wenn Sie einen kennen, würde ich mich über eine Info freuen.2.4 Historie
Letzlich ist dies übrigens ein Effekt, der auf C zurückgeht. Schon in C wurde definiert, dass leere Klammern in einer Typ-Deklaration als Funktion ohne Parameter interpretiert werden[3]. Nur wird dieser Umstand in fast allen Büchern immer verschwiegen. Selbst das extrem empfehlenswerte Buch "Expert C Programmierung" von Peter van der Linden[4] erklärt zwar wunderschön wie man Typ-Deklarationen in C liest - läßt diese Besonderheit aber leider aus.3. Und es geht noch schlimmer
Sie haben wirklich einen Augenblick geglaubt, dass es das war? Tut mir leid, aber es geht noch schlimmer. Stellen Sie sich vor, Sie haben eine Int-Variable "n" und wollen mit dieser einen Char-Variable "c" anlegen.Da Sie natürlich wissen, daß ein "int" größer ist als ein "char", treffen Sie entsprechende Vorsichtsmaßnahmen:
-
Durch die Programm-Logik, durch Abfragen und mit Assertions sichern Sie, dass zur Laufzeit nur Int-Werte auftauchen,
die einen "char" reinpassen.
Hinweis - diesen Teil lassen wir in unserem Beispiel der Übersichtlichkeit halber weg. - Bei der Initialisierung der lokalen Char-Variable "c" casten Sie den Int-Wert auf einen "char", um Ihre Absicht deutlich zu machen und um z.B. auch Compiler-Warnungen zu entgehen. Sie nutzen für den Cast nicht das empfehlenswertere "static_cast" sondern die einfache funktionale Form "char(n)".
int n('A');
char c(char(n)); // (*)
Wahrscheinlich jetzt schon, denn Sie sind ja nicht dumm und haben dazu gelernt. Und Sie wissen dass dieser Artikel Probleme beschreibt. Aber wenn Sie die Zeile (*) in normalem Code gesehen hätten - hätten Sie dort Probleme erwartet? Wahrscheinlich "Nein".
Leider gibt es ein Problem. Das Symbol "c" ist nämlich auch hier keine Variable, sondern wieder eine Funktions-Deklaration - diesmal für eine Funktion, die einen "char" erwartet und einen "char" zurückgibt. Selbst wenn Sie es nicht glauben - der Standard und die Compiler sind da eindeutig.
#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
int n('A'); // (*)
char c(char(n));
cout << typeid(c).name() << endl;
}
Ausgabe: (hier mit dem Microsoft Visual Studio 2012)
char __cdecl(char)
Hinweis - manche Compiler warnen die ungenutzte lokale Variable "n" in Zeile (*) an (siehe z.B. GCC 4.7.2 im nächsten Kapitel 3.1), und liefern damit einen Hinweis auf das Problem. In der Praxis ist meine Erfahrung aber, dass die Nutzer, die diese Warnung bekommen, den Compiler für fehlerhaft halten und nicht Ihren Code.
Mögliche Lösungen sind hier, wenn sie denn zum eingesetzten Typen passen:
- Statt des funktionalen Casts sollte ein "static_cast" bevorzugt werden.
- Elementare Daten-Typen können auch statt mit Klammern auch problemlos mit dem Zuweisungs-Operator initialisiert werden. Aber machen Sie sich klar, dass diese Lösung bei Klassen überflüssige Konstruktor- und Destruktor-Aufrufe beinhalten kann.
- Das Argument kann explizit als temporäre Variable anlegt werden.
- Man kann wieder extra klammern, und macht damit die Interpretation als Funktions-Deklaration ungültig.
#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
int n('A');
char c1(static_cast<char>(n)); // Static-Cast statt funktionalem Cast
cout << typeid(c1).name() << endl;
char c2 = char(n); // Operator = (ist aber nicht immer gut)
cout << typeid(c2).name() << endl;
char c3((char(n))); // Extra Klammern
cout << typeid(c3).name() << endl;
}
Ausgabe: (hier mit dem Microsoft Visual Studio 2012)
char
char
char
3.1 Compiler Warnungen
Einige Compiler warnen genau dieses Problem an, zumindest wenn einige Bedingungen erfüllt sind:- Die Funktions-Deklaration müssen in einem lokalen Kontext stehen
- Es sind keine elementaren Daten-Typen sondern Klassen im Spiel.
#include <iostream>
#include <typeinfo>
using namespace std;
class A
{
public:
explicit A(int) {}
};
class B
{
public:
B(const A&) {}
};
int main()
{
int n = 1;
B b(A(n)); // Fkts-Deklaration von "b"
cout << typeid(b).name() << endl;
}
Ausgabe: (hier mit dem Microsoft Visual Studio 2012)
class B __cdecl(class A)
-
Microsoft Visual Studio 6 (englisch)
--- (keine Warnung) -
Microsoft Visual Studio 2003.NET (deutsch)
Zeile 20 : warning C4930: 'B b(A)': Funktion mit Prototyp wurde nicht aufgerufen
(war eine Variablendefinition gemeint?) -
Microsoft Visual Studio 2010
main.cpp(20): warning C4930: 'B b(A)': prototyped function not called (was a variable definition intended?) -
Microsoft Visual Studio 2012
main.cpp(20): warning C4930: 'B b(A)': Funktion mit Prototyp wurde nicht aufgerufen
(war eine Variablendefinition gemeint?) -
Microsoft Visual Studio 2012 mit Nov 2012 CTP
main.cpp(20): warning C4930: 'B b(A)': prototyped function not called (was a variable definition intended?) -
GCC 4.7.2 (unter Ubuntu Quetzal)
main.cc:19:8 Warnung: Variable "n" wird nicht verwendet [-Wunused-variable] -
Clang 3.0.6 (unter Ubuntu Quetzal)
main.cc:20.7: warning: parantheses were disambiguated as a function declarator -
Comeau Online-Compiler[2] Version 4.3.10.1 Beta
--- (keine Warnung)
4. Typ-Deklarationen gehen vor
Alle drei Probleme haben letztlich den gemeinsamen Grund, dass die C++ Syntax hier nicht eindeutig ist. Alle drei Varianten könnten prinzipiell sowohl als Objekt-Definitionen als auch als Funktions-Deklarationen geparst werden.
// Alles keine lokalen Objekte, sondern Funktions-Deklarationen
int n1(); // -> int (*n1)()
int n2(int()); // -> int (*n2)(int(*)())
int n3(int(x)); // -> int (*n3)(int)
4.1 Ein reales Beispiel
Im sehr empfehlenswerten Buch "Effective STL"[6] beschreibt Scott Meyers in Item 6 ein Beispiel, in dem die letzten beiden Probleme gemeinsam auftreten. Dieses Beispiel ist deshalb besonders ärgerlich, da es kein konstruiertes Beispiel ist, sondern typischen realen Code darstellt.
// Beispiel aus "Effective STL" von Scott Meyers - Item 6
list<int> data(istream_iterator<int>(file), istream_iterator<int>());
Leider aber eben keine wirklich sehr gute Lösung, denn was hier nach einer Listen-Konstruktion aussieht, ist eben in Wirklichkeit eine Funktions-Deklaration:
- Funktions-Name "data"
- Rückgabe einer Liste von "int" via Kopie
- 1. Parameter ein "istream_iterator<int>" mit Namen "file"
- 2. Parameter ein Funktions-Zeiger ohne Parameter mit Rückgabe von "istream_iterator<int>". Dieser Parameter ist unbenannt.
5. C++11
Als vom 6.-14. Juni 2008 beim ISO C++ Standardisierungs-Treffen in Sophia Antipolis, France die Initialisierungs-Listen[7,8,9] als eine allgemeine Syntax für Objekt-Konstruktionen in den mittlerweile aktuellen Standard C++11 aufgenommen wurden, dachte ich: "Nun sind alle Probleme vorbei. Eine einheitliche Syntax für alle Arten von Objekten und Objekt-Konstruktionen - auch in Fällen wo in C++03 gar keine Initialsierungen möglich waren, wie z.B. bei Containern oder C-Arrays in Klassen. Und sie würde alle diese Probleme beseitigen - und eben noch mehr."Ein großer Teil der Hoffnungen haben sich bewahrheitet. Viele Probleme sind mit Initialisierungs-Listen Vergangenheit - so auch unsere:
#include <iostream>
#include <typeinfo>
using namespace std;
class A
{
public:
A() {}
A(int) {}
void fct() { cout << "A::fct()" << endl; }
};
int main()
{
A a1;
A a2{}; // Problem-Fall 1 - so kein Problem
A a3{5};
cout << typeid(a2).name() << endl;
a2.fct();
cout << endl;
// ---
long xyz{int()}; // Problem-Fall 2 - so kein Problem
cout << typeid(xyz).name() << endl;
cout << "xyz: " << xyz << endl;
cout << endl;
// ---
int n{'A'};
char c{char(n)}; // Problem-Fall 3 - so kein Problem
cout << typeid(c).name() << endl;
cout << "c: " << c << endl;
}
Ausgabe: (hier mit dem Microsoft Visual Studio Nov 2012 CTP)
class A
A::fct()
long
xyz: 0
char
c: A
6. Links & Literatur
-
Comeau Online-Compiler
- http://www.comeaucomputing.com/tryitout
- Leider ist der Comeau Online-Compiler, wie die gesamte Comeau Web-Seite, nicht mehr online.
- Das ich die Ergebnisse hier trotzdem zur Verfügung habe liegt daran, dass ich diese Tests schon 2008 mit dem Comeau Online-Compiler durchgeführt habe.
-
Nachzulesen z.B. im ISO C Standard von 1999
- ISO/IEC 9899:1999
- Kapitel 6.7.6 ("Type names"), Fußnote 126
-
Buch "Expert C Programmierung" von Peter van der Linden
- Heise Verlag 1995, 1. Auflage
- ISBN 978-3-88229-047-9
-
ISO C++ Standard von 2003
- ISO/IEC 14882:2003
- Kapitel 8.2 ("Ambiguity resolution")
-
Buch "Effective STL" von Scott Meyers
- Addison-Wesley 2001, 1. Auflage
- ISBN 978-0-201-74962-5
-
N2215, Initializer lists (Rev. 3)
- http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2215.pdf
- Von Bjarne Stroustrup und Gabriel Dos Reis
-
N2672, Initializer List proposed wording
- http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2672.htm
- Von Jason Merrill und Daveed Vandevoorde
-
N2679, Initializer Lists for Standard Containers (Rev. 1)
- http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2679.pdf
- Von Gabriel Dos Reis und Bjarne Stroustrup
-
Probleme in den Initialisierungs-Listen
- Artikel von Malte Skarupke: "The problems with uniform initialization"
- http://probablydance.com/2013/02/02/the-problems-with-uniform-initialization
-
Proposal für Erweiterungen an den Initialisierungs-Listen in C++14 oder C++17
- N3605, Member initializers and aggregates
- http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3605.html
-
Ich bin natürlich nicht der Erste, der diese Probleme beschrieben hat. Einen vergleichbaren Artikel für
Problem 2 hat z.B. Danny Kalev schon 2009 geschrieben.
- Overcoming the "Most Vexing Parse" Problem
- http://www.devx.com/cplus/10MinuteSolution/43032
7. Danksagung
Bedanken möchte ich mich bei folgenden Personen (in alphabetischer Reihenfolge), die diesen Artikel gegengelesen und mit vielen kleinen und großen Hinweisen dafür gesorgt haben, dass er besser ist als am Anfang:- Alexander Heckner
- Klaus Wittlich
- Ralph Habermann
- Robert Wittek
- Sven Johannsen
8. Versions-Historie
Die Versions-Historie dieses Artikels:-
Version 1
- 26.04.2013
- Initiale Version