Development/Tutorials/Common Programming Mistakes (de)

< Development‎ | Tutorials
Revision as of 14:13, 12 December 2007 by DrSlowDecay (Talk | contribs)

Jump to: navigation, search


Contents

Development/Tutorials/Common Programming Mistakes


Häufige Programmierfehler
Anleitungsserie   Grundlagen
Voriges Kapitel   None
Nächstes Kapitel   n/a
Weiterführende Texte   n/a
Navigation   Deutsche Startseite

Zusammenfassung

Diese Anleitung gibt ein paar Tips aus dem Erfahrungsschatz von KDE Entwicklern bezüglich was man in Qt und KDE machen und was lieber sein lassen sollte. Neben aktuellen Fehler behandelt es auch Dinge, die nicht unbedingt als Fehler gelten jedoch den Code langsamer oder weniger lesbar machen.

C++ allgemein

Dieses Kapitel beschäftigt sich mit einigen dunklen Ecken von C++, die gerne falsch benutzt oder schlicht und einfach falsch verstanden werden.

Anonyme Namensräume vs. statics

Wenn Sie eine Methode in einer Klasse haben, die nicht auf Members zugreift und daher kein Objekt zu seiner Arbeit benötigt, machen Sie es static. Wenn diese Methode dann zusätzlich nur eine private Hilfsfunktion ist, welche außerhalb dieser Datei nicht benötigt wird, machen Sie daraus eine file-static Funktion, dadurch wird das Symbol komplett verborgen.

Symbole die in C++ in einem anonymen Namensraum definiert werden, haben keine interne Verknüpfung. Anonyme Namensräume vergeben nur einen eindeutigen Namen für diese Übersetzungseinheit und das ist alles. Die Verknüpfung des Symbols wird überhaupt nicht verändert, weil die zweite Phase einer zwei-Phasen Namenssuche Funktionen mit interner Verknüfung ignoriert. Weiterhin können Elemente mit interner Verknüpfung nicht als Argumente für Templates benutzt werden.

Wenn Sie also nicht wollen, dass ein Symbol exportiert wird, benutzen Sie statt anonymen Namensräumen statische.

NULL Zeiger Probleme

Als erstes und wichtigstes: Es ist OK einen Null Zeiger zu löschen. Daher sind Konstrukte, die einen Zeiger auf Null testen reduntant: if ( ptr ) {

  delete ptr;

}

Beachten Sie jedoch, dass ein Null-Check erforderlich ist, wenn Sie ein Array löschen - das liegt daran, dass ein relativ neuer Compiler auf Solaris-Systemen das anderweitig nicht richtig behandelt.

Wenn Sie einen Zeiger löschen, stellen Sie sicher, dass sie ihn auf 0 setzen, so dass zukünftige Löschversuche nicht mit einer doppelten Löschung fehlschlagen. Der entsprechende Code dazu:

delete ptr; ptr = 0;

You may notice that null pointers are marked variously in one of three ways: 0, 0L and NULL. In C, NULL is defined as a null void pointer. However, in C++, this is not possible due to stricter type checking. Therefore, modern C++ implementations define it to a "magic" null pointer constant which can be assigned to any pointer. Older C++ implementations, OTOH, simply defined it to 0L or 0, which provides no additional type safety - one could assign it to an integer variable, which is obviously wrong.

In pointer context, the integer constant zero means "null pointer" - irrespective of the actual binary representation of a null pointer. This means that the choice between 0, 0L and NULL is a question of personal style and getting used to something rather than a technical one - as far as the code in KDE's SVN goes you will see 0 used more commonly than NULL.

Note, however, that if you want to pass a null pointer constant to a function in a variable argument list, you *must* explicitly cast it to a pointer - the compiler assumes integer context by default, which might or might not match the binary representation of a pointer. Again, it does not matter whether you cast 0, 0L or NULL, but the shorter representation is generally preferred.

Member Variablen

Sie werden vielleicht vier Hauptstile bemerkt haben, wie in KDE-Klassen Member-Variablen benannt werden:

  • m_variable kleines m, Unterstrich und der Name der Variable mit einem Kleinbuchstaben am Anfang. Das ist der am meisten verwendete Stil und wird im Code von kdelibs bevorzugt.
  • mVariable kleines m und der Name der Variable mit einem Großbuchstaben am Anfang
  • variable_ Name der Variable mit einem Kleinbuchstaben am Anfang und variable einem Unterstrich am Ende
  • _variable Unterstrich und der Name der Variable mit einem Kleinbuchstaben am Anfang. Dieser Stil wird nicht so gerne gesehen, da diese Notation auch in Code für Funktionsparameter verwendet wird.

Wie immer gibt es nicht nur einen korrekten Weg es zu machen, so folgen Sie einfach der Syntax der Applikation/Bibliothek zu welcher Sie Code ergänzen.

Static variablen

Try to limit the number of static variables used in your code, especially when committing to a library. Construction and initialization of large number of static variables really hurts the startup times.

Do not use class-static variables, especially not in libraries and loadable modules though it is even discouraged in applications. Static objects lead to lots of problems such as hard to debug crashes due to undefined order of construction/destruction.

Instead, use a static pointer, together with K_GLOBAL_STATIC which is defined in kglobal.h and is used like this:

class A { ... };

K_GLOBAL_STATIC(A, globalA)

void doSomething() {

    A *a = globalA;
    ...

}

void doSomethingElse() {

   if (globalA.isDestroyed()) {
       return;
   }
   A *a = globalA;
   ...

}

void installPostRoutine() {

   qAddPostRoutine(globalA.destroy);

}

See the API documentation for K_GLOBAL_STATIC for more information.

Forward Deklarationen

Die Übersetzungszeit kann reduziert werden, indem man Klassen forward deklariert anstatt den entsprechenden Header einzubinden. Ein Beispiel:

  1. include <QWidget> // langsam
  2. include <QStringList> // langsam
  3. include <QString> // langsam

class SomeInterface { public:

   virtual void widgetAction( QWidget *widget ) =0;
   virtual void stringAction( const QString& str ) =0;
   virtual void stringListAction( const QStringList& strList ) =0;

};

Das obige sollte statt dessen folgendermaßen formuliert werden:

class QWidget; // schnell class QStringList; // schnell class QString; // schnell class SomeInterface { public:

   virtual void widgetAction( QWidget *widget ) =0;
   virtual void stringAction( const QString& str ) =0;
   virtual void stringListAction( const QStringList& strList ) =0;

};

Iteratoren

Prefer to use const_iterators over normal iterators when possible. Containers, which are being implicitly shared often detach when a call to a non-const begin() or end() methods is made (List is an example of such a container). When using a const_iterator also watch out that you are really calling the const version of begin() and end(). Unless your container is actually const itself this probably will not be the case, possibly causing an unnecessary detach of your container. So basically whenever you use const_iterator initialize them using constBegin()/constEnd() instead, to be on the safe side.

Cache the return of the end() method call before doing iteration over large containers. For example:

QValueList<SomeClass> container;

//code which inserts a large number of elements to the container

QValueListConstIterator end( container.end() );

for ( QValueListConstIterator itr( container.begin() );

    itr != end; ++itr ) {

}

This avoids the unnecessary creation of the temporary end() return object on each loop iteration, largely speeding it up.

Prefer to use pre-increment over post-increment operators on iterators as this avoids creating an unnecessary temporary object in the process.

take care when erasing elements inside a loop

When you want to erase some elements from the list, you maybe would use code similar to this:

QMap<int, Job *>::iterator it = m_activeTimers.begin(); QMap<int, Job *>::iterator itEnd = m_activeTimers.end();

for( ; it!=itEnd ; ++it ) {

   if(it.value() == job)
   {
       //A timer for this job has been found. Let's stop it.
       killTimer(it.key());
       m_activeTimers.erase(it);
   }

}

This code will potentially crash because it is a dangling iterator after the call to erase(). You have to rewrite the code this way: QMap<int, Job *>::iterator it = m_activeTimers.begin(); while (it != m_activeTimers.end()) {

   QMap<int, Job *>::iterator prev = it;
   ++it;
   if(prev.value() == job)
   {
       //A timer for this job has been found. Let's stop it.
       killTimer(prev.key());
       m_activeTimers.erase(prev);
   }

} This problem is also discussed in the Qt documentation for QMap::iterator but applies to all Qt iterators

Programm Design

In this section we will go over some common problems related to the design of Qt/KDE applications.

Verspätete Initialisierung

Although the design of modern C++ applications can be very complex, one recurring problem, which is generally easy to fix, is not using the technique of delayed initialization.

First, let us look at the standard way of initializing a KDE application:

int main( int argc, char **argv ) {

   ....
   KApplication a;
   KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
   MainWindow *window = new MainWindow( args );
   a.setMainWidget( window );
   window->show();
   return a.exec();

}

Notice that window is created before the a.exec() call that starts the event loop. This implies that we want to avoid doing anything non-trivial in the top-level constructor, since it runs before we can even show the window.

The solution is simple: we need to delay the construction of anything besides the GUI until after the event loop has started. Here is how the example class MainWindow's constructor could look to achieve this:

MainWindow::MainWindow() {

   initGUI();
   QTimer::singleShot( 0, this, SLOT(initObject()) );

}

void MainWindow::initGUI() {

   /* Construct your widgets here.  Note that the widgets you
    * construct here shouldn't require complex initialization
    * either, or you've defeated the purpose.
    * All you want to do is create your GUI objects and
    * QObject::connect
    * the appropriate signals to their slots.
    */

}

void MainWindow::initObject() {

   /* This slot will be called as soon as the event loop starts.
    * Put everything else that needs to be done, including
    * restoring values, reading files, session restoring, etc here.
    * It will still take time, but at least your window will be
    * on the screen, making your app look active.
    */

}

Using this technique may not buy you any overall time, but it makes your app seem quicker to the user who is starting it. This increased perceived responsiveness is reassuring for the user as they get quick feedback that the action of launching the app has succeeded.

When (and only when) the start up can not be made reasonably fast enough, consider using a KSplashScreen.

Daten Strukturen

In this section we will go over some of our most common pet-peeves which affect data structures very commonly seen in Qt/KDE applications.

non-POD typen übergeben

Non-POD ("plain old data") types should be passed by const reference if at all possible. This includes anything other than the basic types such as char and int.

Take, for instance, QString. They should always be passed into methods as const QString&. Even though QString is implicitly shared it is still more efficient (and safer) to pass const references as opposed to objects by value.

So the canonical signature of a method taking QString arguments is:

void myMethod( const QString & foo, const QString & bar );

QObject

Sollten Sie jemals eine von QObject abgeleitete Klasse aus den eigenen Methoden heraus löschen sollen, löschen Sie es nicht folgendermaßen:

  delete this;

Das wird früher oder später in einen Absturz führen, da eine Methode dieses Objektes aus der Qt-Ereignisschleife heraus via slots/signals aufgerufen werden könnte, nachdem Sie es gelöscht haben.

Nutzen Sie statt dessen immer QObject::deleteLater(), was das gleiche wie delete this erledigt jedoch in einer sicheren Art und Weise.

Leere QStrings

Häufig muss geprüft werden, ob ein QString leer ist. Hier sind drei Ansätze das zu prüfen, wobei nur die ersten beiden korrekt sind:


// Richtig if ( mystring.isEmpty() ) { }

// Richtig if ( mystring == QString() ) { }

// Falsch! "" if ( mystring == "" ) { }

Obwohl es ein Unterschied zwischen "null" QStrings und leeren gibt, ist das nur ein historisches Artefakt, neuer Code sollte das nicht mehr benutzen.

QString und das Lesen aus Dateien

If you are reading in a file, it is faster to convert it from the local encoding to Unicode (QString) in one go, rather than line by line. This means that methods like QIODevice::readAll() are often a good solution, followed by a single QString instantiation.

For larger files, consider reading a block of lines and then performing the conversion. That way you get the opportunity to update your GUI. This can be accomplished by reentering the event loop normally, along with using a timer to read in the blocks in the background, or by creating a local event loop.

While one can also use qApp->processEvents(), it is discouraged as it easily leads to subtle yet often fatal problems.

QString aus einem KProcess lesen

KProcess löst das Signal readyReadStandard{Output|Error} aus, sobald Daten eintreffen.

Ein häufiger Fehler ist es, alle verfügbaren Daten im verbundenen Slot zu lesen und sofort in ein QString zu konvertieren: Die Daten könnten in kleineren unregelmäßigen Portionen einreffen, daher könnten multi-byte Zeichen in Stücke geschnitten und daher ungültig werden. Mehrere Ansätze existieren:

  • Müssen Sie die Daten wirklich sofort verarbeiten sobald sie eintreffen? Wenn nicht, benutzen Sie lieber readAllStandard{Output|Error} nachdem der Prozess beendet wurde. Anders als in KDE3 ist KProcess nun in der Lage, die Daten zu sammeln.
  • Sammeln Sie die Datenbrocken im Slot und bearbeiten Sie sie immer dann, wenn ein Newline kommt oder ein bestimmter Timeout auftritt.Example code (KDE3 based)
  • Fügen Sie den Prozess in ein QTextStream ein und lesen Sie diesen zeilenweise. Während das zumindest theoretisch funktionieren sollte, funktioniert das in der Praxis derzeit nicht.

QString und QByteArray

Während QString das Werkzeug der Wahl für viele Situationen ist, in denen mit Zeichenketten gearbeitet wird, gibt es eine Situation, wo dies äußerst ineffizient ist. Wenn Sie mit Daten arbeitet, die in einem QByteArray gespeichert sind, sehen Sie sich vor, dass Sie diese nicht an Methoden übergeben, die einen QString Parameter benötigen und daraus wieder ein QByteArray macht.

For example:

QByteArray myData; QString myNewData = mangleData( myData );

QString mangleData( const QString& data ) {

   QByteArray str = data.toLatin1();
   // mangle 
   return QString(str);

}

Der kostspielige Teil hier ist die Konvertierung nach QString, was intern als Konvertierung in Unicode durchgeführt wird. Das ist unnötig, da das erste, was die Methode tut, eine Rückkonvertierung nach toLatin1() ist. Wenn Sie sich also sicher sind, dass eine Unicode Konvertierung nicht bennötigt wird, versuchen sich Zwischenschritte mit QString zu vermeiden.

Das obige Beispiel sollte statt dessen geschrieben werden als:

QByteArray myData; QByteArray myNewData = mangleData( myData );

QByteArray mangleData( const QByteArray& data )

QDomElement

When ein XML Dokument geparst wird, benötigt man häufig eine Iteration über alle Elemente. Man könnte versucht sein, folgenden Code dafür zu benutzen:

for ( QDomElement e = baseElement.firstChild().toElement();

     !e.isNull();
     e = e.nextSibling().toElement() ) {
      ...

}

Das ist jedoch nicht korrekt: Die obige Schleife wird vorzeitig beendet wenn es auf einen QDomNode trifft, der etwas anderes als ein Element ist (zum Beispiel ein Kommentar).

Die korrekte Schleife sieht so aus:

for ( QDomNode n = baseElement.firstChild(); !n.isNull();

     n = n.nextSibling() ) {
   QDomElement e = n.toElement();
   if ( e.isNull() ) {
       continue;
   }
   ...

}


KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V.Legal