C++Guns – RoboBlog blogging the bot

23.11.2017

C++ Guns: RE: Abstraction design and implementation: `repeat` Teil 2

Filed under: Allgemein — Tags: — Thomas @ 19:11

In Teil 1 ging es um die allgemeinen Probleme einer simplen for() Schleife.
In Teil 2 werde ich Vittorio Romeo Ansatz verfolgen, eine repeat() Funktion zu erstellen, die eine Funktion f genau N mal aufruft.

Syntaktisch und semantisch würde ich diesen Code okay finden:

#include <type_traits>
#include <iostream>

void f() {
  cout << "f()\n";
}

template<typename Func>
void repeat(int n, Func&& f) {
  for(int i=0; i < n; ++i) {
    f();
  }
}

int main() {
  repeat(10, f);
}

Die Funktion f wird einfach 10 mal aufgerufen. Das war einfach.

Sobald ich mehr tun will, wirds kompliziert.

Schritt für Schritt. Wir werden sehen wann es nicht mehr klappt.
Möchte ich der Funktion f einen Parameter mitgeben, der nicht von der aktuellen Iterationen abhängt, lässt sich das leicht mit einem Lambda realisieren, welches ein Interface darstellt. Dieses Interface bildet meine Funktion f(int) auf eine Funktion ohne Argument ab. Also genau so eine Funktion, welche repeat() erwartet.

void f(int parameter) {
  cout << "f() parameter " << parameter << "\n";
}

int main() {
  int parameter = 42;
  repeat(10, [parameter]() {
    f(parameter);
  });
}

Die Funktion repeat() ändert sich nicht. Das ist schon einmal ein gutes Zeichen.
Jetzt wird es kritisch. Ich möchte neben dem Parameter noch die Laufvariablen der repeat Funktion mit nach f() übergeben. Dafür benutze ich wieder ein Lambda welches das Interface bereit stellt und auch if constexpr und std::is_invocable_v. Also hight end C++17 Shit :D Mal sehn obs kappt.

void f(int i, int parameter) {
  cout << "f() i: " << i << " parameter: " << parameter << "\n";
}

template<typename Func>
void repeat(int n, Func&& f) {
  for(int i=0; i < n; ++i) {
    
    if constexpr(std::is_invocable_v<Func&&, int>) {
      f(i);
    } else {
      f();
    }
  }
}

int main() {
  int parameter = 42;
  repeat(10, [parameter](int i) {
    f(parameter, i);
  });
}

Der Code compiliert mit einem C++17 Compiler (gcc 7) und liefert folgende Ausgabe:


f() i: 42 parameter: 0
f() i: 42 parameter: 1
f() i: 42 parameter: 2
f() i: 42 parameter: 3
f() i: 42 parameter: 4
f() i: 42 parameter: 5
f() i: 42 parameter: 6
f() i: 42 parameter: 7
f() i: 42 parameter: 8
f() i: 42 parameter: 9

Das ist nicht das, was ich wollte. Ich habe absichtlich die Funktions Argumente parameter und i vertauscht und der Compiler hats nicht gemerkt! Wie sollte er auch. Die Variable i ist nur ein int Type und hat keinerlei Semantik einer Laufvariablen.

Das hat mich am Artikel "abstraction design and implementation: `repeat`" auch so stark gestört. Damit es ist super einfach lazy Bugs zu erzeugen und am Ende hat man nichts gewonnen.

Im dritten Teil will ich versuchen das C++ Typesystem zu nutzen, um die Semantik einer Laufvariablen auszudrücken und im letzten Beispiel einen Compilerfehler zu erzwingen.

C++ Guns: RE: Abstraction design and implementation: `repeat` Teil 1

Filed under: Allgemein — Tags: — Thomas @ 17:11

Hallo,
ich möchte hier ein paar Gedanken äußern zu dem tollen Artikel "abstraction design and implementation: `repeat`" von Vittorio Romeo [1].

Sein Ziel ist es eine simple for() Schleife, wie wir sie schon alle hingeschrieben haben, zu nehmen und zu verbessern. Das Problem bei einer voll ausgeschriebenen for() Schleife ist, dass sie zeigt WAS alles gemacht wird: Eine Schleifenvariable declakiert, initialisiert, incrementiert. Aber was willen wir eigentlich damit aussagen? Na, rufe die Funktion f() einfach 10 mal auf. Ahsoo!

for(int i=0; i < 10; ++i) {
    foo();
}

Das Problem an allen Codestücken die bis heute geschrieben worden sind, ist dass ihnen die Semantik fehlt. Der Code sagt immer nur auf, WIE etwas gerade verarbeitet wird, aber nicht WAS eigentlich getan werden soll. Wenn man Glück hat, steht das WAS in irgendeiner veralteten Dokumentation oder Kommentarzeilen die sich irgendwo befinden.

Aber in diesem Blogpost will ich nicht lehren zu Dokumentieren, sondern die Leute sensibel dafür machen, dass sie das, WAS sie wollen, auch genauso in Code ausdrücken können.

Kommen wir zurück zur for() Schleife.
In dem Code Beispiel oben steckt ziemlich viel low-level Technik. Es wird erst einmal eine Schleifenvariable i vom Typ int deklariert und mit 0 initialisiert. Und Stopp. Hier sind schon zwei lazy Bugs passiert.
Wer sagt denn, dass der Typ int sein soll? Wenn man über std::vector o.ä. Iteriert, sollte der Typ size_t sein, da std::vector::size() auch size_t zurück liefert. Und auch operator[] und at() jeweils ein size_t als Argument erwarten. Wenn ich eine Schleife aber nur 10mal laufen lassen will, dann ist der Typ int wiederum meist in Ordnung.

Die Initialisierung mit 0 ist auch nicht zwangsläufig richtig. Zwar fängt der Index in C/C++ bei 0 an, aber wenn man eine Schleife nur 10mal laufen lassen will, ist es doch egal ob man bei 0 anfängt und bis 9 zählt, oder ob man bei 1 startet und bei 10 endet.

Das bringt uns gleich zum nächsten lazy Bug. Der Ausdruck i < 10 ist so nicht intuitiv. Der Mensch beginnt nun mal bei 1 ab zu zählen und endet bei 10. Daher schreiben viele, noch nicht gefolterte C++ Programmierer, zurecht i <= 10.

Der nächste lazy Bug kommt noch in der selben Code Zeile. Ist es nun ++i oder i++? In diese Beispiel wäre es egal, da int ein POD ist. Ansonsten gilt, dass ++i vorzuziehen ist, da keine temporäre Variable erstellt (und evtl. wieder weg optimiert) werden muss.

Der letzte lazy Bug sehe ich leider auch sehr oft. Die Variable i wird nicht innerhalb der for() Schleife deklariert, sondern nur innerhalb der Funktion. Also altes C/Fortran. So ist es möglich, absichtlich oder aus versehen, die Variable i mehrmals zu nutzen. In der Regel ist dies nicht gewollt, und wenn doch, wird es nicht dokumentiert.

Es kann nicht sein, dass auf so viele Sachen geachtet werden muss, wenn eine simple Schleife gefordert ist. Vittorio Romeo zeigt zurecht wie es einfacher geht. Leider macht er so die Sache nicht besser, nur anders schlimm.

Teil 2

[1] https://vittorioromeo.info/index/blog/abstraction_design_implementation_repeat.html

02.11.2017

C++ Guns - Addressen von Referenzen

Filed under: Allgemein — Tags: — Thomas @ 12:11

Haben Referenzen in C++ eigene Adressen, so wie normale Variablen und Pointer, oder haben sie nur die Adresse der referenzierten Variablen?
Wenn Referenzen eigene Adressen besitzen, existieren sie wie normale Variablen im RAM. Ich meine mich aber erinnert zu haben, dass eine Referenz nur ein Alias für eine andere Variable ist. Einfach nur syntactic sugar für den Programmierer. Das wäre natürlich optimal, da unnötige Kopieraktionen entfallen.
Ein kleines Experiment bringt Klarheit:

void func(int& c) {
  cout << "In func(): Adress of c reference to a: " << addressof(c) << "\n";
  c = 1;
}

int main() {
  int a = 0;
  cout << "Value of a: " << a << "\n";
  cout << "Adress of a: " << addressof(a) << "\n";
  int &b = a;
  cout << "Adress of b reference to a: " << addressof(b) << "\n";
  func(a);
  cout << "Value of a: " << a << "\n";
}
Value of a: 0                                                                                                                                                                       
Adress of a: 0x7ffd58cf22b4                                                                                                                                                         
Adress of b reference to a: 0x7ffd58cf22b4                                                                                                                                          
In func(): Adress of c reference to a: 0x7ffd58cf22b4                                                                                                                               
Value of a: 1

Es gibt nur eine einzige Variable im Programm, mit der Adresse 0x7ffd58cf22b4. Es ist egal, ob über eine Referenz darauf zugegriffen wird, oder sogar aus einer Funktion heraus, über eine Referenz, die Variable a manipuliert wird. Es werden nur 4Byte Speicher im RAM belegt. Wunderbar.

Mit Pointer sollte dieser Effekt nicht zu erreichen sein. Da Pointer ja echte Variablen mit eigenen Adressen.

void func(int* c) {
  cout << "In func(): Adress of c pointer to a: " << addressof(c) << "\n";
  *c = 1;
}

int main() {
  int a = 0;
  cout << "Value of a: " << a << "\n";
  cout << "Adress of a: " << addressof(a) << "\n";
  int* b = addressof(a);
  cout << "Adress of b pointer to a: " << addressof(b) << "\n";
  func(addressof(a));
  cout << "Value of a: " << a << "\n";
}
Value of a: 0
Adress of a: 0x7ffc955db71c
Adress of b pointer to a: 0x7ffc955db710
In func(): Adress of c pointer to a: 0x7ffc955db6e8
Value of a: 1

Ganz klar zu sehen, jeder Pointer hat eine andere Adresse als Variable a. Es werden also 3*4Byte = 12Byte Speicher im RAM belegt. Plus zusätzliche Kopieraktionen der Adresse von a hin zur Pointervariablen.

Interessanterweise sind Referenzen im C Sprachstandard nicht vorgesehen. Somit ist es nicht möglich, dieses simple Beispiel auf Laufzeit hin optimiert in C zu programmieren. Nur in C++.

Powered by WordPress