Konstanten in Arduino/C++

Für wen ist dieser Artikel gedacht?

In einfachen Arduino-Sketches ist es eigentlich egal, wie man seine Konstanten definiert, wichtig ist zunächst nur Konsistenz. Wenn man jedoch auch etwas professionelleren Code schreiben möchte ist es gut, die Vor- und Nachteile der verschiedenen Optionen zu kennen.

Arduino-Sketches sind C++-Code, welcher um Arduino-spezifische Funktionen erweitert wurde. Sämtliche C++(11)-Sprachfeatures sind im Code erlaubt. Daher gibt es oft viele Wege, einen bestimmten Effekt zu erzielen. Je nach Anwendungsfall gibt es dann manchmal idiomatische und nicht-idiomatische Wege. Dieser Artikel behandelt drei verschiedene Möglichkeiten, konstante Werte im Code zu definieren und zu verwenden.

define.ino
#define LED_PIN 13
#define DELAY_TIME 500
 
void setup() {
    pinMode(LED_PIN, OUTPUT);
}
 
void loop() {
    digitalWrite(LED_PIN, HIGH);
    delay(DELAY_TIME);
    digitalWrite(LED_PIN, LOW);
    delay(2 * DELAY_TIME);
}

#define ist eine Präprozessor-Anweisung. Alle nachfolgenden Verwendungen von LED_PIN werden vor dem Kompilieren durch 13 ersetzt.

Vorteile

  • Kein zusätzlicher Speicher benötigt, da Ersetzung direkt beim Funktionsaufruf

Nachteile

  • Man kann die Definition mittels #undef später im Programm wieder zurücknehmen und neu definieren. Die Konstante hätte dann im ganze Programm nicht überall denselben Wert. Klar kann man sich sagen, dass man das einfach nicht macht, aber sich solch ein Regelwerk aufzusetzen und dann womöglich im Team noch durchzusetzen ist immer eher suboptimal. Es ist immer besser, wenn man so programmiert, dass schon der Compiler unsinnigen Code erkennt.
  • Diese Option bringt keine Typinformationen mit sich und führt nur triviale Textersetzung durch. Man könnte ja auch #define DELAY_TIME 400 + 100 schreiben, was dann später im Programm zu nicht sofort offensichtlichen Fehlern führt (2 * 400 + 100 ist definitiv nicht dasselbe wie 2 * 500). Man kann in #defines auch Code verstecken der bei jeder Verwendung ausgeführt wird, diese Möglichkeiten verleiten oft zu schwer lesbarem Code.
const.ino
const int led_pin { 13 };
 
void setup() {
    pinMode(led_pin, OUTPUT);
}
 
void loop() {
    digitalWrite(led_pin, HIGH);
    delay(500);
    digitalWrite(led_pin, LOW);
    delay(500);
}

const int led_pin { 13 }; ???

Diese Schreibweise nennt sich C++ uniform initialization. Sie ist fast immer besser als led_pin = 13, weil C++ auch folgenden Code erlauben würde:

const int led_pin = 13.5;

Das ist natürlich definitiv falsch. Daher gibt es seit C++11 die erstgenannte Schreibweise, bei der kein implizites type-narrowing vorgenommen wird.

const int led_pin { 13.5 };

gibt nämlich einen Kompilierfehler.

Vorteile

  • Typsicherheit: Konstanten haben auch einen Typ
  • Sicherheit gegen Veränderungen: Konstanten können nicht verändert/undefiniert werden (Ja, man kann manchmal das const weg-casten, aber das betrachten wir hier nicht)

Nachteile

  • Globale Variablen und Konstanten landen im heap Speicher und belegen für die gesamte Programmlaufzeit diesen Platz. Ob und wie der Compiler sie wegoptimiert hängt stark von den verwendeten Compileroptimierungen ab.
constexpr.ino
constexpr int led_pin { 13 };
constexpr int delay_time { 500 };
 
void setup() {
    pinMode(led_pin, OUTPUT);
}
 
void loop() {
    digitalWrite(led_pin, HIGH);
    delay(delay_time);
    digitalWrite(led_pin, LOW);
    delay(2 * delay_time);
}

constexpr vereint die Vorteile der beiden vorherigen Optionen.

Vorteile

  • Typsicherheit: Konstanten haben auch einen Typ
  • Sicherheit gegen Veränderungen: Konstanten können nicht verändert/undefiniert werden
  • Kein zusätzlicher Speicher benötigt, da Ersetzung durch Compiler
  • Man kann sogar constexpr int delay_time { 400 + 100 }; schreiben und es funktioniert trotzdem immer noch alles wie erwartet, da der Compiler die Anweisung zur Compilezeit auswertet

Nachteile

  • Das Syntax-Highlighting der Arduino-IDE kennt constexpr nicht als Schlüsselwort
  • Nicht jeder kennt die „neue“ C++11-Syntax (die zum Zeitpunkt dieses Artikel ja auch erst offiziell 7 Jahre alt ist)

Man kann sogar einige Funktionen constexpr machen, um diese schon zur Compilezeit auszuwerten, darauf gehe ich hier aber jetzt nicht ein. Im Arduino-Kontext wird das meistens auch nicht gebraucht.