Constexpr vs Makros

93

Wo sollte ich Makros bevorzugen und wo sollte ich constexpr bevorzugen ? Sind sie nicht im Grunde gleich?

#define MAX_HEIGHT 720

vs.

constexpr unsigned int max_height = 720;
Tom Dorone
quelle
4
AFAIK constexpr bietet mehr Typensicherheit
Code-Apprentice
14
Einfach: constexr, immer.
n. 'Pronomen' m.
Könnte
Ortwin Angermeier

Antworten:

151

Sind sie nicht im Grunde gleich?

Nein auf keinen Fall. Nicht einmal annähernd.

Abgesehen davon , ist das Makro ein intund Sie constexpr unsignedein unsigned, gibt es wichtige Unterschiede und Makros nur haben einen Vorteil.

Umfang

Ein Makro wird vom Präprozessor definiert und bei jedem Auftreten einfach in den Code eingesetzt. Der Präprozessor ist dumm und versteht die C ++ - Syntax oder -Semantik nicht. Makros ignorieren Bereiche wie Namespaces, Klassen oder Funktionsblöcke, sodass Sie keinen Namen für andere Elemente in einer Quelldatei verwenden können. Dies gilt nicht für eine Konstante, die als richtige C ++ - Variable definiert ist:

#define MAX_HEIGHT 720
constexpr int max_height = 720;

class Window {
  // ...
  int max_height;
};

Es ist in Ordnung, eine Mitgliedsvariable aufzurufen, max_heightda sie ein Klassenmitglied ist und daher einen anderen Bereich hat und sich von dem im Namespace-Bereich unterscheidet. Wenn Sie versuchen würden, den Namen MAX_HEIGHTfür das Mitglied wiederzuverwenden , würde der Präprozessor ihn in diesen Unsinn ändern, der nicht kompiliert werden würde:

class Window {
  // ...
  int 720;
};

Aus diesem Grund müssen Sie Makros angeben, UGLY_SHOUTY_NAMESum sicherzustellen, dass sie hervorstechen, und Sie können vorsichtig sein, wenn Sie sie benennen, um Konflikte zu vermeiden. Wenn Sie Makros nicht unnötig verwenden, müssen Sie sich darüber keine Sorgen machen (und müssen nicht lesenSHOUTY_NAMES ).

Wenn Sie nur eine Konstante in einer Funktion haben möchten, können Sie dies nicht mit einem Makro tun, da der Präprozessor nicht weiß, was eine Funktion ist oder was es bedeutet, sich in ihr zu befinden. Um ein Makro nur auf einen bestimmten Teil einer Datei zu beschränken, müssen Sie #undefes erneut ausführen:

int limit(int height) {
#define MAX_HEIGHT 720
  return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}

Vergleichen Sie mit dem weitaus vernünftigeren:

int limit(int height) {
  constexpr int max_height = 720;
  return std::max(height, max_height);
}

Warum bevorzugen Sie das Makro?

Ein echter Speicherort

Eine constexpr-Variable ist eine Variable also tatsächlich im Programm vorhanden ist, und Sie können normale C ++ - Dinge tun, z. B. ihre Adresse nehmen und einen Verweis darauf binden.

Dieser Code hat ein undefiniertes Verhalten:

#define MAX_HEIGHT 720
int limit(int height) {
  const int& h = std::max(height, MAX_HEIGHT);
  // ...
  return h;
}

Das Problem ist, dass MAX_HEIGHTes sich nicht um eine Variable handelt, sodass der Aufruf std::maxeines temporären intElements vom Compiler erstellt werden muss. Die Referenz, die von zurückgegeben wird, std::maxverweist dann möglicherweise auf die temporäre Referenz , die nach dem Ende dieser Anweisung nicht mehr vorhanden ist, sodass return hauf ungültigen Speicher zugegriffen wird.

Dieses Problem besteht bei einer richtigen Variablen einfach nicht, da sie einen festen Speicherort hat, der nicht verschwindet:

int limit(int height) {
  constexpr int max_height = 720;
  const int& h = std::max(height, max_height);
  // ...
  return h;
}

(In der Praxis würden Sie wahrscheinlich int hnicht erklären , const int& haber das Problem kann in subtileren Kontexten auftreten.)

Präprozessorbedingungen

Die einzige Zeit, um ein Makro zu bevorzugen, ist, wenn Sie möchten, dass sein Wert vom Präprozessor verstanden wird, um ihn unter #ifBedingungen zu verwenden, z

#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif

Sie konnten hier keine Variable verwenden, da der Präprozessor nicht versteht, wie Variablen namentlich referenziert werden. Es versteht nur grundlegende sehr grundlegende Dinge wie Makroerweiterung und Direktiven, die mit #(wie #includeund #defineund #if) beginnen.

Wenn Sie eine Konstante wünschen, die vom Präprozessor verstanden werden kann, sollten Sie den Präprozessor verwenden, um sie zu definieren. Wenn Sie eine Konstante für normalen C ++ - Code wünschen, verwenden Sie normalen C ++ - Code.

Das obige Beispiel soll nur eine Präprozessorbedingung demonstrieren, aber selbst dieser Code könnte die Verwendung des Präprozessors vermeiden:

using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
Jonathan Wakely
quelle
3
Eine constexprVariable muss erst Speicher belegen, wenn ihre Adresse (ein Zeiger / eine Referenz) verwendet wurde. Andernfalls kann es vollständig optimiert werden (und ich denke, es könnte Standardese geben, das dies garantiert). Ich möchte dies betonen, damit die Leute den alten, minderwertigen " enumHack" nicht aus einer fehlgeleiteten Vorstellung heraus weiter verwenden, dass ein Trivial constexpr, das keinen Speicher benötigt, dennoch einige davon besetzt.
underscore_d
3
Ihr Abschnitt "Ein realer Speicherort" ist falsch: 1. Sie geben nach Wert (int) zurück, sodass eine Kopie erstellt wird. Das temporäre ist kein Problem. 2. Wenn Sie als Referenz (int &) zurückgekehrt int heightwären, wäre dies genauso ein Problem wie das Makro, da sein Umfang an die Funktion gebunden ist, im Wesentlichen auch vorübergehend. 3. Der obige Kommentar "const int & h verlängert die Lebensdauer des temporären" ist korrekt.
PoweredByRice
4
@underscore_d true, aber das ändert nichts am Argument. Die Variable benötigt keinen Speicher, es sei denn, sie wird odr verwendet. Der Punkt ist, dass, wenn eine echte Variable mit Speicher benötigt wird, die Variable constexpr das Richtige tut.
Jonathan Wakely
1
@PoweredByRice 1. Das Problem hat nichts mit dem Rückgabewert von zu tun. limitDas Problem ist der Rückgabewert von std::max. 2. Ja, deshalb wird keine Referenz zurückgegeben. 3. falsch, siehe den coliru Link oben.
Jonathan Wakely
3
@PoweredByRice seufz, du musst mir wirklich nicht erklären, wie C ++ funktioniert. Wenn Sie den Wert haben const int& h = max(x, y);und maxum diesen zurückgeben, verlängert sich die Lebensdauer des Rückgabewerts. Nicht nach dem Rückgabetyp, sondern nach dem, an den const int&es gebunden ist. Was ich geschrieben habe, ist richtig.
Jonathan Wakely
11

Im Allgemeinen sollten constexprSie Makros verwenden, wann immer Sie möchten, und nur dann, wenn keine andere Lösung möglich ist.

Begründung:

Makros sind eine einfache Ersetzung im Code und führen aus diesem Grund häufig zu Konflikten (z. B. Windows.h- maxMakro vs std::max). Darüber hinaus kann ein funktionierendes Makro leicht auf andere Weise verwendet werden, was dann seltsame Kompilierungsfehler auslösen kann. (z.BQ_PROPERTY für Strukturelemente verwendet)

Aufgrund all dieser Unsicherheiten ist es ein guter Codestil, Makros zu vermeiden, genau wie Sie normalerweise Gotos vermeiden würden.

constexpr ist semantisch definiert und erzeugt daher typischerweise weit weniger Probleme.

Adrian Maire
quelle
1
In welchem ​​Fall ist die Verwendung eines Makros unvermeidlich?
Tom Dorone
3
Bedingte Kompilierung unter Verwendung von #ifDingen, für die der Präprozessor tatsächlich nützlich ist. Das Definieren einer Konstante ist nicht eines der Dinge, für die der Präprozessor nützlich ist, es sei denn, diese Konstante muss ein Makro sein, da sie unter Präprozessorbedingungen verwendet wird #if. Wenn die Konstante für die Verwendung in normalem C ++ - Code (keine Präprozessoranweisungen) vorgesehen ist, verwenden Sie eine normale C ++ - Variable, kein Präprozessor-Makro.
Jonathan Wakely
Abgesehen von der Verwendung variadischer Makros, meistens der Verwendung von Makros für Compiler-Switches, ist es jedoch eine gute Idee, aktuelle Makroanweisungen (wie bedingte Switches, String-Literal-Switches) zu ersetzen, die sich mit echten Code-Anweisungen mit constexpr befassen.
Ich würde sagen, dass Compiler-Switches auch keine gute Idee sind. Ich verstehe jedoch voll und ganz, dass es manchmal benötigt wird (auch Makros), insbesondere wenn es um plattformübergreifenden oder eingebetteten Code geht. Um Ihre Frage zu beantworten: Wenn Sie bereits mit Präprozessor zu tun haben, würde ich Makros verwenden, um klar und intuitiv zu halten, was Präprozessor und was Kompilierungszeit ist. Ich würde auch vorschlagen, stark zu kommentieren und die Verwendung so kurz und lokal wie möglich zu gestalten (vermeiden Sie Makros, die sich um oder 100 Zeilen #if verbreiten). Vielleicht ist die Ausnahme der typische # ifndef-Schutz (Standard für #pragma einmal), der gut verstanden wird.
Adrian Maire
3

Tolle Antwort von Jonathon Wakely . Ich würde Ihnen auch raten, sich die Antwort von jogojapan anzusehen, um herauszufinden , was der Unterschied zwischen constund istconstexpr bevor Sie überhaupt über die Verwendung von Makros nachdenken.

Makros sind dumm, aber auf gute Weise. Angeblich sind sie heutzutage eine Build-Hilfe, wenn Sie möchten, dass ganz bestimmte Teile Ihres Codes nur kompiliert werden, wenn bestimmte Build-Parameter "definiert" werden. Normalerweise bedeutet dies nur, dass Sie Ihren Makronamen verwenden, oder besser noch, nennen wir ihn a Triggerund fügen Dinge hinzu wie /D:Trigger:-DTrigger etc. zu den Build - Tools verwendet werden.

Obwohl es viele verschiedene Verwendungszwecke für Makros gibt, sind dies die beiden, die ich am häufigsten sehe und die keine schlechten / veralteten Praktiken sind:

  1. Hardware- und plattformspezifische Codeabschnitte
  2. Erhöhte Ausführlichkeit baut auf

Während Sie im Fall des OP das gleiche Ziel erreichen können, ein int mit constexproder a zu definieren MACRO, ist es unwahrscheinlich, dass sich die beiden bei Verwendung moderner Konventionen überschneiden. Hier sind einige allgemeine Makronutzungen, die noch nicht eingestellt wurden.

#if defined VERBOSE || defined DEBUG || defined MSG_ALL
    // Verbose message-handling code here
#endif

Nehmen wir als weiteres Beispiel für die Verwendung von Makros an, dass Sie eine bevorstehende Hardware veröffentlichen müssen oder eine bestimmte Generation davon, die einige knifflige Problemumgehungen enthält, die die anderen nicht benötigen. Wir definieren dieses Makro als GEN_3_HW.

#if defined GEN_3_HW && defined _WIN64
    // Windows-only special handling for 64-bit upcoming hardware
#elif defined GEN_3_HW && defined __APPLE__
    // Special handling for macs on the new hardware
#elif !defined _WIN32 && !defined __linux__ && !defined __APPLE__ && !defined __ANDROID__ && !defined __unix__ && !defined __arm__
    // Greetings, Outlander! ;)
#else
    // Generic handling
#endif
kayleeFrye_onDeck
quelle