Programming with C++
Eigene Anmerkungen und Beispiele sind ohne weitere Kennzeichnung enthalten
// MyClass.h
// Important domain specific constants at namespace level
const size_t MY_MAX_LEN = 2345;
// Class specific constants
class MyClass
{
// constant integral value can be directly provided within header
static const int MY_INT = 333;
// constant non integral value is provided within cpp file
static const double MY_DOUBLE;
// providing non integral value directly through function,
// no additional entry within cpp file needed
static const char* MyName() {return "AnyString";}
};
// MyClass.cpp
const int MyClass::MY_INT; // definition here is required!
// for const value see header
const double MyClass::MY_DOUBLE = 2.34;
// File: MyThirdPartyInclude.h // always include this file and not the third party include directly #pragma warning(push) // disbale for this header only #pragma warning(disable:4512) // disable specific warning #include "ThirdPartyInclude.h" #pragma warning(pop) // restore original warning level
Neben Funktionsparametern sollten auch lokale Variablen, die einmalig ermittelt werden, als const definiert werden:
void DoSomething()
{
int const LENGTH = CalculateLength();
// ... now we know: length will remain unchanged
// throughout the whole function
}
class MyResource // may be file, port, ...
{
// constructor allocates resource (e.g. open file)
MyResource(const string & in_identification);
// destructor frees resource (e.g. close file)
~MyResource();};
// Example: using as automated stack object
void DoSomething()
{
MyResource myResource("NameOfResource");
//...
// use resource
//...
} // at end of scope resource is freed automatically
// Refcounted resource handling
shared_ptr<MyResource> resource = new MyResource("SomeResource");
// Resource is freed when the last shared pointer referring to it goes away
Zur Vermeidung von Problemen bei
möglicherweise auftretenden Exceptions in jeder Zeile Code / in jedem Statement
nur maximal eine Resource allokieren und dem Verwaltungsobjekt zuweisen
(sonst könnten mehrere Resourcen erzeugt werden und noch bevor sie an die zugehörigen
Resourcemanagementobjekte übergeben werden, könnte eine Exception das korrekte Verwalten
und abschliessende Aufräumen unterbinden)mögliche Probleme: Aufwand zur Erzeugung temporärer Objekte, versehentlicher Aufruf anderer Funktionen mit evtl. unerwarteter Signatur
Empfehlung: Konstruktoren, die ein einzelnes Argument akzeptieren als explicit definieren, erforderliche Umwandlungen besser über Konvertierungsfunktionen zur Verfügung stellen (asChar(), toLong())
"It is far, far easier to make a correct program fast
than it is to make a fast program correct."
// within global namespace or some named namespace dont do that:
int someNumber;
string someString ("AnyContent");
void SomeFunction(){...};
Stattdessen sollen nur die Deklarationen aufgeführt sein:
extern int someNumber;
extern string someString; // definition (string contents) see cpp file
void SomeFunction(); // extern not needed, definition (implementation body)
// see cpp file
Static-Definitionen beseitigen zwar Linker-Meldungen zu doppelt-definierten
Symbolen, führen aber zur Vervielfachung der entsprechenden CodeTeile und
ggf. auch zu gar nicht beabsichtigten logischen Effekten (gleichnamige statische Variable existiert für jedes Cpp-Modul als eigene Instanz und erlaubt keinen Zugriff von mehreren Modulen aus).
Negativbeispiel
static int someNumber;
static string someString("AnyContent");
static void SomeFunction(){...};
Empfehlung: Im Headerfile keine static-Elemente auf Namespace-Level definieren.
Ausnahme: Inline-Funktionen haben external linkage, werden aber nur einmal angelegt.
object.NewFunction dann
NewFunction(object)
Ziel:
neue Anforderungen sollen durch neuen Code realisiert werden,
der bestehende Code soll dabei möglichst nicht verändert werden
Ziel:
Für den Anwender kann die neue Klasse wie die zugehörige Basisklasse angesteuert
werden. Üblicherweise erfolgt die Interaktion über einen Zeiger auf die Basisklasse.
Public inheritance muss das Liskov Substitution Prinzip erfüllen, d.h. alle wesentlichen Verhaltensweisen der Basisklasse müssen erfüllt werden.
Jede überschriebene Basisfunktion
class Base
{
virtual void SomeFunction(int);
virtual void SomeFunction(int,int);
void SomeFunction(int,int,int);
};
class Derived : public Base
{
virtual void SomeFunction(int); // overrides Base::SomeFunction
// and hides the other two base functions
};
Hiding durch eine using-Deklaration vermeiden:
class Derived : public Base
{
virtual void SomeFunction(int); // overrides Base::SomeFunction
using Base::SomeFunction; // make the other two base functions visible
};
Default-Argumente unverändert übernehmen (sonst werden je nach statischem Typ der
Verwendung unterschiedliche Werte eingesetzt).
Andere Motivationen für nonmember-Funktionen: linkes Argument
ist ungleich dem Klassentyp (z.B. bei Streamoperastoren) oder
soll implizite Typ-Umwandlungen unterstützen.
Bei Bedarf kann die Funktion zum nonmember friend der Klasse gemacht werden.
Virtuelles Verhalten durch Aufruf einer virtuellen Memberfunktion
aus der nonmember Funktion:
class MyBaseClass
{
public:
// pure base method to write contents to stream
virtual std::ostream& WriteToStream (std::ostream &) const = 0;
};
// non member and non virtual function,
// virtual behaviour comes from the second argument
std::ostream& operator<<(std::ostream & in_rStream, MyBaseClass const & in_rBase)
{
return in_rBase.WriteToStream (in_rStream);
}
class MyClass
{
// public methods forwarding to implementation part
// ...
private:
struct MyImpl;
shared_ptr<MyImpl> m_pMyImpl;
};
Aufwand für Pimpl jedoch nur dann betreiben, wenn durch die Verkapselung ein tatsächlicher Vorteil entsteht.
Negativbeispiel:
SomeFunction (++n, ++n);
int in_numValues
MyUserType const & in_rMyData
Kanonische Form für Arithmetische/Assignment Operatoren
Ein Operator @ (Platzhalter für +,-,*,/) sollte stets folgende Eigenschaften erfüllen:
T& T::operator@= (const T &)
{
//...
return *this;
}
T operator@ (T const & lhs, T const & rhs)
{
T temp (lhs);
return temp @= rhs;
}
//Hat bereits die Basisklasse einen spezifischen new-Operator, so müssen abgeleitete Klassen diesen verfügbar machen:plain new void* operator new (std::size_t); // nothrow new void* operator new (std::size_t, std::nothrow_t) throw(); // in-place new // (is often used by STL containers) void* operator new (std::size_t, void*)
class Derived : public Base
{
public:
using Base::operator new;
};
Will man bei der Konstruktion einer Klasse virtuelles Verhalten so gibt es folgende Möglichkeiten:
class Base
{
protected:
Base(){};
virtual void Init();
public
template<class T>
static shared_ptr<T> Create()
{
shared_ptr<T> p (new T);
p->Init();;
return p;
}
};
class A
{
private:
// disable copying
A (const A&); // not implemented
A& operator=(A const &); // not implemented
};
Derartige Klassen können dann nicht in Standard-Containern verwaltet
werden. Bei Bedarf ist jedoch Verwaltung über SmartPointer möglich.
A& operator= (A const &); // traditional signature A& operator= (A); // optimizer friendlyZiele: error-safe, möglichst "strong guarantee", safe for self-assignment
A& A::operator= (A const & rhs) // traditional signature
{
A temp (rhs);
swap (temp);
return *this;
}
A& A::operator= (A rhs) // rhs passed by value, optimizer friendly
{
swap (rhs);
return *this;
}
Normalerweise ist polymorphes Verhalten erwünscht. Je nach beabsichtigter/ versehentlicher Wahl der Funktionssignaturen besteht die Gefahr ungewollter Beschränkung auf die Basisfunktionalität:
class Derived : Base {...};
DoSomething (Base in_base); // value param
DoSomethingElse (Base& in_base); // reference param
Derived aDerivedObject;
DoSomething (aDerivedObject); // object is sliced to Base type!
DoSomethingElse (aDerivedObject); // polymorphic access to Derived type
Lösung über Clone-Funktion:
class Base
{
public:
Base* Clone() const // nonvirtual interface function to be
{ // used by all client code
Base* pBase = DoClone();
// Ensure that derived class has correctly defined DoClone
// and does not return a wrong type
assert (typeid(*pBase) == typeid(*this)) &&
"DoClone incorrectly overridden"
return pBase;
}
protected:
// disable copying
Base (Base const &):
private:
// Every derived (and also more derived) class has to
// implement this method
virtual Base* DoClone() const = 0;
};
Mit diesem Ansatz führt der (versehentliche) Aufruf von DoSomething (aDerivedObject); zu einem Compile-Error. Unvollständig realisierte abgeleitete Klassen
haben zumindest einen Laufzeitfehler mit assert.
Hinweis: typeid und polymorphic classes
typeid liefert für polymorphe Klassen den Laufzeit-Typ zurück, auch wenn ein Basisklassenzeiger
übergeben wird. Damit eine Klasse polymorphes Verhalten zeigt, muss sie mindestens eine
virtuelle Methode besitzen (sonst liefert typeid nur den statischen Typ des übergebenen Parameters):
class Base
{
// polymorphic behaviour is activated if
// at least one virtual function exists
virtual void SomeFunction() {};
};
class Derived : public Base
{
};
...
Base base;
Derived derived;
Base* pBase = &derived;
Derived* pDerived = &derived;
std::cout << "typeid(pBase) = " << typeid(*pBase).name() << std::endl;
std::cout << "typeId(pDerived) = " << typeid(*pDerived).name() << std::endl;
Output:
typeid(pBase) = class MyNamespace::Derived
typeId(pDerived) = class MyNamespace::Derived
When the polymorphic behaviour is removed (e.g. by
removing virtual method Base::SomeFunction) the output changes to:
typeid(pBase) = class MyNamespace::Base
typeId(pDerived) = class MyNamespace::Derived
class A
{
public:
void swap (A& rhs)
{
m_dataSpecial.MySpecificSwap (rhs.dataSpecial);
std::swap (m_dataPrimitive, rhs.m_dataPrimitive);
std::swap (m_dataStdContainer, rhs.m_dataStdContainer);
}
private:
SpecialClass m_dataSpecial; // class providing specific swap function
int m_dataPrimitive;
std::vector<double> m_dataStdContainer;
};
Darauf aufbauend kann z.B. auch der Assignment-Operator realisiert werden
(siehe Swap Idiom).
Wenn SpecialClass keine sichere Swap-Funktion anbietet, jedoch no fail copy construction und assignment, so kann std::swap verwendet werden.
Existiert kein sicherer Copy Constructor, so kann SpecialClass als SmartPointer
in der Klasse aufgenommen werden, da Zeiger sicher ausgetauscht werden können.
(Das Kopieren der zugehörigen Zeigerinhalte ist nicht erforderlich, da Swap
lediglich das Austauschen des Bezugs erfordert.)
Empfehlung:
Spezialisierung von std::swap (nur möglich für Nicht-Template-Klassen)
namespace std
{
template<> void swap (MyClass& lhs, MyClass& rhs)
{
lhs.swap(rhs);
}
}
Allgemein: nonmember Funktion im gleichen namespace.
Das logische Interface einer Klasse X besteht sowohl aus den
public member functions (z.B. void X::MyFunction1 (void))
als auch aus den
nonmember functions (z.B. void MyFunction2(X in_x)),
die den Typ X verwenden und im gleichen namespace wie X definiert
sind.
Zur Unterstützung dieses Interfaceprinzips wurde in C++ das "argument dependent lookup (ADL)" oder "Koenig lookup" eingeführt, das dafür sorgt, dass die passenden nonmember-Funktionen genauso einfach gefunden werden wie reguläre member functions.
namespace MyNamespace
{
class X
{
public:
void MyFunction1 (void);
};
void MyFunction2 (X in_x);
}
Ähnliche Verwendung im Client-Code:
X x; x.MyFunction1(); MyFunction2(x);Insbesondere bei Operator Funktionen (z.B. auch Stream-Operatoren) sollten nonmember functions bevorzugt werden:
namespace MyNamespace
{
class X {};
X operator+ (X const&, X const &);
// => enables simple client syntax: xSum = x1 + x2;
}
Empfehlung
Bevorzugt sollten Operatoren und Funktionen als nonmember nonfriend
Funktionen realisiert werden (also nicht als reguläre Memberfunktionen).
Motivation: Minimierung der Abhängigkeiten, der Functionbody kann dann nicht
vom nicht öffentlichen Teil der Klasse abhängen.
Vorsicht
Nonmember-Funktionen, die nicht zum Interface von X gehören, dürfen
nicht im gleichen Namespace definiert werden. Insbesondere gilt das
für alle Templatefunktionen. Sonst besteht die Gefahr, dass sie durch das ADL
für nicht vorgesehene Zwecke (z.B. von X abgeleitete Typen wie
std::vector<X>) verwendet werden.
Negativbeispiel
Mögliche Reihenfolgenprobleme, die z.B. durch unterschiedliche Includereihenfolgen
ausgelöst werden können:
// code section 1
namespace A
{
int f (double);
}
...
// code section 2
namespace B
{
using A::f; // only considers f(double)!
void g();
}
...
// code section 3
namespace A
{
int f (int); // is not considered by the preceding using declaration!
}
...
// code section 4
void B::g{)
{
f(1); // calls A::f(double)
// if code section 3 would preced section 2
// it would call the better matching function A::f(int)
}
template<typename T>
void MyTemplateFunction (T in_t)
{
MyHelperNamespace::MyFunction(in_t);
// alternative: parenthesis also disables ADL
(MyFunction)(in_t);
}
template<typename T>
class C : Base<T>
{
// safe access to dependent type within base
typedef typename Base<T>::SomeType MyType;
void Function ()
{
// safe access to base member function
Base<T>::SomeBaseFunction();
// same effect via this pointer, but allowing virtual behaviour
this->SomeBaseFunction();
}
};
// (within template code) // SomeFunction and value_type are customization points: t.SomeFunction(); // regular member function syntax typedef typename T::value_type MyType;
// (within template code) SomeFunction(t); // unqualified call to non member function typedef typename T::value_type MyType;
// (within template code) SomeTraits<T>::SomeFunction(t); typedef typename SomeTraits<T>::value_type MyType;
assert(!"Some description to display in case of error");
assert ( (someValue==43)
&& (!"Some description to display in case of"
"preceding condition is not fulfilled"));
Hinweis:
Die Verletzung einer Precondition bei Modul-/Projektinternem Aufruf
der Funktion kann auch als Programmierfehler gesehen werden,
der mit assert() überwacht werden kann.
throw; statt über
throw e; ausgeführt werden, damit der Polymorphismus erhalten
bleibt.
catch (MyException & e) // catch by reference to non-const when changes
{ // of the exception object are needed
e.DoSomethingToChangeObject(); // e.g. add context info to error chain
throw; // rethrow changed exception object
}
Mögliche Motivation: In manchen Anwendungsfällen hat man keine Kontrolle über die Compiler-Optionen mit denen bestimmte Module generiert werden. Damit sind die ExceptionHandling-Mechanismen evtl. gar nicht binär kompatibel.
Ein try-catch(...)-Block sollte deshalb in folgenden Situationen eingesetzt werden:
Zeiger auf Arraybereich: &*myVector.begin() oder &myVector.front()
char*-Lesezugriff: myString.c_str() liefert null-terminierten C-String,
char*-Schreibzugriff: myString.data() liefert nicht null-terminierten String
z.B. auto_ptr<T> ist nicht geeignet, da beim Kopieren der Objekt-Bezug evtl. an temporäre Container-Elemente weitergegeben wird
Nicht-Value-Typen oder polymorphe Objekte können über smart pointer verwaltet werden
myVector.insert(position, itFirst, itLast)
// reduce capacity to minimum container<T>(myContainer).swap(myContainer); // clear all contents and reduce capacity to minimum container<T>().swap(myContainer);
myContainer.erase(std::remove(myContainer.begin(), myContainer.end(),
someValue), myContainer.end());
Hinweis: der remove-Algorithmus arbeitet nur mit dem Iterator und hat keinen Zugriff auf den Container,
kann also nichts daraus löschen.
Begründung:
Negativbeispiel: Löschen jedes dritten Elementes nicht so realisieren:
v.erase(remove_if(v.begin(),v.end(),CheckIfCalledNumTimes(3)));
// predicate function object struct HasSomeProperty : public unary_function{ bool operator() (SomeThing const &) const {...} } // compare function object struct MyCompare : public binary_function { bool operator() (SomeThing const &, SomeThing const &) const {...} }
Hinweis: Für sehr einfache Fälle sind herkömlich geschriebene Schleifen weiterhin die einfachste und lesbarste Lösung.
(Negativ-)Beispiel für handgeschriebene Schleife zur Ermittlung des ersten Elementes, das zwischen minVal und maxVal liegt:
vector<int>::iterator it = v.begin();
for(;it != v.end(); ++it)
{
if (*it > minVal && *it < maxVal) break;
}
Negativbeispiel für schwer lesbare Lösung über Standard-Binders:
vector<int>::iterator it = find_if(v.begin(),v.end(),
compose2 ( std::logical_and<bool>(),
std::bind2nd(greater<int>(),minVal),
std::bind2nd(less<int>(),maxVal)));
Negativbeispiel für selbstgeschriebenes Function-Objekt, das weit entfernt von der Schleife definiert ist:
template<typename T>
class Between : public unary_function<T,bool>
{
public:
Between (T const & in_minVal, T const & in_maxVal)
: m_minVal (in_minVal), m_maxVal (in_maxVal) {}
bool operator()(T const & in_val) const // compare logic far away from loop
{return in_val > m_minVal && in_val < m_maxVal;}
private:
T m_minVal;
T m_maxVal;
};
...
vector<int>::iterator it = find_if(v.begin(),v.end(), Between<int>(minVal, maxVal);
Empfehlung: Verwendung von (Boost) Lambda-Funktionen:
vector<int>::iterator it = find_if(v.begin(),v.end(), _1 > minVal && _1 < maxVal);siehe auch BOOST_FOREACH
Hinweis: binary_search liefert lediglich die Info, ob ein Element existiert oder nicht und ist meist nutzlos.
Beispiel: Zahlen größer als 7 nach vorne sortieren:
partition (numbers.begin(), numbers.end(), std::bind2nd(std::greater<int>(),7);
Beispiel 1: die 5 größten Zahlen nach vorne sortieren
nth_element (numbers.begin(), numbers.begin() + 4, numbers.end(), std::greater<int>());
Beispiel 2: die Person mit dem mittleren Gewicht (= Median) bestimmen,
links davon die leichteren, rechts die schwereren Personen
nth_element (people.begin(), people.begin() + people.size()/2, people.end(), WeightIsLess);
Beispiel: die 3 kleinsten Zahlen in aufsteigender Reihenfolge nach vorne sortieren
partial_sort (numbers.begin(), numbers.begin() + 3, numbers.end(), std::less<int>());
Instead of writing:
for (MyContainer::iterator itElement = myContainer.begin();
itElement != myContainer.end(); ++itElement)
{
it->ChangeState();
}
you can simply write:
BOOST_FOREACH (MyElement & element, myContainer)
{
element.ChangeState();
}
std::list<int> numberList;
// Build sum of all positive numbers within list";
int sum (0);
BOOST_FOREACH (int curInt, numberList)
{
if (curInt > 0)
sum+=curInt;
}
cout << "nSum=" << sum << endl;
// Increment all numbers within the map
// Remark: As BOOST_FOREACH is a macro you cannot use types
// containing commas
// instead use typedefs to get simpler and more readable names
typedef std::map<std::string,int> NumberMap;
BOOST_FOREACH (NumberMap::value_type & curPair, numberMap)
{
++curPair.second;
}
struct MyDataStruct
{
int m_id;
double m_val;
std::string m_name;
SpIDemoInterface m_spIDemoInterface;
void WriteName();
void WriteNameConst() const;
};
typedef boost::shared_ptr<MyDataStruct const> SpMyDataStructConst;
std::vector<SpMyDataStructConst> myDatas;
//Calling const member functions and accessing an interface pointer
// stored in the struct
BOOST_FOREACH (SpMyDataStructConst spCurStruct, myDatas)
{
// access data member and interface method
cout << "nStructname=" << spCurStruct->m_name << "n";
spCurStruct->m_spIDemoInterface->DoSomethingConst();
// call const member function
spCurStruct->WriteNameConst();
// Cannot call non const member function
// spCurStruct->WriteName();
// Remark: Calling non const method within interface member is possible
spCurStruct->m_spIDemoInterface->DoSomething();
}
unsigned int numCurElement = distance(myContainer.begin(),itCurrentElement)
+ 1;unsigned int numElementsLeft = distance(itCurrentElement, myContainer.end());Siehe MSDN
std::listvalues; int sum; std::for_each(values.begin(), values.end(), [&sum](int const & in_value){if (in_value > 0) sum+=in_value;}); std::cout "nSum= " sum << std::endl;
Siehe MSDN
SomeComplexContainerType values;
BOOST_FOREACH(auto element, values)
{
// do something with element
}
The big problems:
Example: "ABC" is given by the code points U+0041 U+0042 U+0043
As the numeric range of hexadecimal numbers is unlimited also the number of representable characters is conceptually unlimited.
The BOM to be used for UTF-16 on any computer system is always 0xFEFF. Depending on the endianess of your computer system the bytes will be stored in order FE FF (big endian) or FF FE (little endian). The compuer system reading an file (possibly received from an external system) can now read the BOM and deduce about the stored byte order of the UTF-16 chars.
std::wstring fileContents = GetFromSomeWhere(); // Build file path as wstring std::wstring fileDir = L"C:/SomeDir"; std::wstring fileName = L"MyUTF16Sample.txt"; std::wstring fullFilePath = fileDir + L"/" + fileName; // Open/create UNICODE file FILE* targetFile = _wfopen(fullFilePath .c_str(), L"wb, ccs=UNICODE"); std::wofstream ofFile(targetFile); // Write byte order mark const wchar_t BOM_UTF_16 = 0xFEFF; ofFile << BOM_UTF_16; // Write text contents ofFile.write(fileContents.c_str(), fileContents.length()); ofFile.close();