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;
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;
Antworten:
Sind sie nicht im Grunde gleich?
Nein auf keinen Fall. Nicht einmal annähernd.
Abgesehen davon , ist das Makro ein int
und Sie constexpr unsigned
ein unsigned
, gibt es wichtige Unterschiede und Makros nur haben einen Vorteil.
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_height
da 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_HEIGHT
fü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_NAMES
um 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 #undef
es 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?
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_HEIGHT
es sich nicht um eine Variable handelt, sodass der Aufruf std::max
eines temporären int
Elements vom Compiler erstellt werden muss. Die Referenz, die von zurückgegeben wird, std::max
verweist dann möglicherweise auf die temporäre Referenz , die nach dem Ende dieser Anweisung nicht mehr vorhanden ist, sodass return h
auf 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 h
nicht erklären , const int& h
aber das Problem kann in subtileren Kontexten auftreten.)
Die einzige Zeit, um ein Makro zu bevorzugen, ist, wenn Sie möchten, dass sein Wert vom Präprozessor verstanden wird, um ihn unter #if
Bedingungen 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 #include
und #define
und #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>;
constexpr
Variable 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 " enum
Hack" nicht aus einer fehlgeleiteten Vorstellung heraus weiter verwenden, dass ein Trivial constexpr
, das keinen Speicher benötigt, dennoch einige davon besetzt.
int height
wä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.
limit
Das Problem ist der Rückgabewert von std::max
. 2. Ja, deshalb wird keine Referenz zurückgegeben. 3. falsch, siehe den coliru Link oben.
const int& h = max(x, y);
und max
um 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.
Im Allgemeinen sollten constexpr
Sie Makros verwenden, wann immer Sie möchten, und nur dann, wenn keine andere Lösung möglich ist.
Makros sind eine einfache Ersetzung im Code und führen aus diesem Grund häufig zu Konflikten (z. B. Windows.h- max
Makro 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.
#if
Dingen, 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.
Tolle Antwort von Jonathon Wakely . Ich würde Ihnen auch raten, sich die Antwort von jogojapan anzusehen, um herauszufinden , was der Unterschied zwischen const
und 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 Trigger
und 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:
Während Sie im Fall des OP das gleiche Ziel erreichen können, ein int mit constexpr
oder 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