Pomijając fakt, że Twoje makro to, inta Twoje constexpr unsignedjest unsigned, istnieją istotne różnice, a makra mają tylko jedną zaletę.
Zakres
Makro jest definiowane przez preprocesor i jest po prostu podstawiane do kodu za każdym razem, gdy występuje. Preprocesor jest głupi i nie rozumie składni ani semantyki języka C ++. Makra ignorują zakresy, takie jak przestrzenie nazw, klasy lub bloki funkcyjne, więc nie możesz użyć nazwy dla niczego innego w pliku źródłowym. Nie dotyczy to stałej zdefiniowanej jako właściwa zmienna w C ++:
Dobrze jest mieć wywoływaną zmienną składową, max_heightponieważ jest składową klasy, a więc ma inny zakres i różni się od zmiennej w zakresie przestrzeni nazw. Gdybyś spróbował ponownie użyć nazwy MAX_HEIGHTdla członka, preprocesor zmieniłby ją na ten nonsens, który się nie kompiluje:
classWindow {// ...int720;
};
Dlatego musisz podać makra, UGLY_SHOUTY_NAMESaby się wyróżniały, i możesz zachować ostrożność podczas nazywania ich, aby uniknąć kolizji. Jeśli nie używasz makr niepotrzebnie, nie musisz się o to martwić (i nie musisz czytać SHOUTY_NAMES).
Jeśli chcesz mieć stałą wewnątrz funkcji, nie możesz tego zrobić za pomocą makra, ponieważ preprocesor nie wie, czym jest funkcja ani co to znaczy być w niej. Aby ograniczyć makro tylko do określonej części pliku, musisz to #undefzrobić ponownie:
Zmienna constexpr jest zmienną, więc faktycznie istnieje w programie i możesz robić normalne rzeczy w C ++, takie jak pobranie jej adresu i powiązanie z nią odwołania.
Problem polega na tym, że MAX_HEIGHTnie jest to zmienna, więc wywołanie zmiennej std::maxtymczasowej intmusi zostać utworzone przez kompilator. Odwołanie, które jest zwracane przez std::maxmoże wtedy odnosić się do tego tymczasowego, który nie istnieje po zakończeniu tej instrukcji, więc return huzyskuje dostęp do nieprawidłowej pamięci.
Ten problem po prostu nie istnieje z odpowiednią zmienną, ponieważ ma ona stałą lokalizację w pamięci, która nie znika:
(W praktyce prawdopodobnie byś tego int hnie powiedział, const int& hale problem może pojawić się w bardziej subtelnych kontekstach.)
Warunki preprocesora
Jedynym momentem, w którym preferujesz makro, jest sytuacja, gdy potrzebujesz zrozumienia jego wartości przez preprocesor, do użycia w #ifwarunkach, np.
Nie możesz użyć tutaj zmiennej, ponieważ preprocesor nie rozumie, jak odwoływać się do zmiennych według nazwy. Rozumie tylko podstawowe, bardzo podstawowe rzeczy, takie jak rozwijanie makr i dyrektywy zaczynające się od #(jak #includei #definei #if).
Jeśli potrzebujesz stałej, która może być zrozumiana przez preprocesor , powinieneś użyć preprocesora do jej zdefiniowania. Jeśli chcesz mieć stałą dla normalnego kodu C ++, użyj normalnego kodu C ++.
Powyższy przykład ma na celu jedynie zademonstrowanie warunku preprocesora, ale nawet ten kod mógłby uniknąć użycia preprocesora:
using height_type = std::conditional_t<max_height < 256, unsignedchar, unsignedint>;
constexprPotrzeba zmienna nie zajmują pamięci aż do jego adres (wskaźnik / odniesienia) jest pobierana; w przeciwnym razie można go całkowicie zoptymalizować (i myślę, że może istnieć Standardese, który to gwarantuje). Chcę to podkreślić, aby ludzie nie kontynuowali używania starego, gorszego „ enumhackowania” z błędnego pomysłu, że trywialny constexpr, który nie wymaga pamięci, mimo wszystko zajmie trochę.
underscore_d
3
Twoja sekcja „Prawdziwa lokalizacja pamięci” jest nieprawidłowa: 1. Wracasz według wartości (int), więc kopia jest tworzona, tymczasowa nie stanowi problemu. 2. Gdybyś wrócił przez odniesienie (int &), int heightbyłby to taki sam problem jak makro, ponieważ jego zakres jest powiązany z funkcją, zasadniczo również tymczasowy. 3. Powyższy komentarz „const int & h przedłuży żywotność tymczasowego” jest poprawny.
PoweredByRice
4
@underscore_d true, ale to nie zmienia argumentu. Zmienna nie będzie wymagała przechowywania, chyba że zostanie użyta odr. Chodzi o to, że gdy wymagana jest rzeczywista zmienna z pamięcią, zmienna constexpr działa właściwie.
Jonathan Wakely
1
@PoweredByRice 1. problem nie ma nic wspólnego ze zwracaną wartością limit, problemem jest zwracana wartość std::max. 2. tak, dlatego nie zwraca odwołania. 3. źle, zobacz link coliru powyżej.
Jonathan Wakely
3
@PoweredByRice westchnij, naprawdę nie musisz mi wyjaśniać, jak działa C ++. Jeśli masz const int& h = max(x, y);i maxzwraca przez wartość, okres życia wartości zwracanej jest wydłużony. Nie przez typ zwracany, ale przez typ, z const int&którym jest powiązany. To co napisałem jest poprawne.
Jonathan Wakely
11
Ogólnie rzecz biorąc, powinieneś używać, constexprkiedy tylko możesz, a makr tylko wtedy, gdy żadne inne rozwiązanie nie jest możliwe.
Racjonalne uzasadnienie:
Makra są prostym zamiennikiem w kodzie iz tego powodu często generują konflikty (np. maxMakro windows.h vs std::max). Dodatkowo działające makro można łatwo wykorzystać w inny sposób, co może wywołać dziwne błędy kompilacji. (np. Q_PROPERTYużywany na elementach konstrukcji)
Z powodu wszystkich tych niepewności dobrym stylem kodu jest unikanie makr, dokładnie tak, jak zwykle unikasz goto.
constexpr jest zdefiniowana semantycznie i dlatego zazwyczaj generuje znacznie mniej problemów.
Kompilacja warunkowa przy użyciu #ifrzeczy, do których preprocesor jest faktycznie przydatny. Definiowanie stałej nie jest jedną z rzeczy, do których preprocesor jest przydatny, chyba że ta stała musi być makrem, ponieważ jest używana w warunkach preprocesora przy użyciu #if. Jeśli stała jest używana w normalnym kodzie C ++ (a nie w dyrektywach preprocesora), użyj normalnej zmiennej C ++, a nie makra preprocesora.
Jonathan Wakely
Z wyjątkiem używania makr wariadycznych, głównie makr do przełączników kompilatora, ale dobrym pomysłem jest próba zastąpienia bieżących instrukcji makr (takich jak warunkowe, przełączniki literałów łańcuchowych) zajmujących się rzeczywistymi instrukcjami kodu za pomocą constexpr?
Powiedziałbym, że przełączniki kompilatora też nie są dobrym pomysłem. Jednak w pełni rozumiem, że jest to potrzebne czasami (także makra), szczególnie w przypadku kodu wieloplatformowego lub osadzonego. Odpowiadając na twoje pytanie: Jeśli masz już do czynienia z preprocesorem, użyłbym makr, aby jasno i intuicyjnie określić, czym jest preprocesor i co to jest czas kompilacji. Sugerowałbym również obszerny komentarz i uczynienie go jak najkrótszym i jak najbardziej lokalnym (unikaj makr rozprzestrzeniających się wokół lub 100 linii #if). Może wyjątkiem jest typowy strażnik #ifndef (kiedyś standard dla #pragma), który jest dobrze zrozumiany.
Adrian Maire
3
Świetna odpowiedź Jonathona Wakely'ego . Radziłbym również zapoznać się z odpowiedzią jogojapan, aby dowiedzieć się, jaka jest różnica między, consta constexprnawet zanim zaczniesz rozważać użycie makr.
Makra są głupie, ale w dobry sposób. Pozornie w dzisiejszych czasach są one pomocą przy kompilacji, gdy chcesz, aby bardzo konkretne części twojego kodu były kompilowane tylko w obecności określonych parametrów kompilacji, które zostały „zdefiniowane”. Zazwyczaj wszystkie środki, które bierze swoją nazwę makra, albo jeszcze lepiej, nazwijmy to się Triggeri dodając rzeczy podoba /D:Trigger, -DTriggeritp do narzędzi budowania używane.
Chociaż istnieje wiele różnych zastosowań makr, są to dwa, które najczęściej widzę, a które nie są złe / przestarzałe:
Sekcje kodu specyficzne dla sprzętu i platformy
Buduje się zwiększona gadatliwość
Więc chociaż możesz w przypadku OP osiągnąć ten sam cel, jakim jest zdefiniowanie int z constexprlub a MACRO, jest mało prawdopodobne, że te dwa elementy będą się pokrywać, gdy używasz nowoczesnych konwencji. Oto kilka typowych zastosowań makr, które nie zostały jeszcze wycofane.
#if defined VERBOSE || defined DEBUG || defined MSG_ALL// Verbose message-handling code here#endif
Jako kolejny przykład użycia makr, powiedzmy, że masz nadchodzący sprzęt do wydania lub może jego konkretną generację, która ma trudne obejścia, których inne nie wymagają. Zdefiniujemy to makro jako 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
Odpowiedzi:
Nie, absolutnie nie. Nawet nie blisko.
Pomijając fakt, że Twoje makro to,
int
a Twojeconstexpr unsigned
jestunsigned
, istnieją istotne różnice, a makra mają tylko jedną zaletę.Zakres
Makro jest definiowane przez preprocesor i jest po prostu podstawiane do kodu za każdym razem, gdy występuje. Preprocesor jest głupi i nie rozumie składni ani semantyki języka C ++. Makra ignorują zakresy, takie jak przestrzenie nazw, klasy lub bloki funkcyjne, więc nie możesz użyć nazwy dla niczego innego w pliku źródłowym. Nie dotyczy to stałej zdefiniowanej jako właściwa zmienna w C ++:
#define MAX_HEIGHT 720 constexpr int max_height = 720; class Window { // ... int max_height; };
Dobrze jest mieć wywoływaną zmienną składową,
max_height
ponieważ jest składową klasy, a więc ma inny zakres i różni się od zmiennej w zakresie przestrzeni nazw. Gdybyś spróbował ponownie użyć nazwyMAX_HEIGHT
dla członka, preprocesor zmieniłby ją na ten nonsens, który się nie kompiluje:class Window { // ... int 720; };
Dlatego musisz podać makra,
UGLY_SHOUTY_NAMES
aby się wyróżniały, i możesz zachować ostrożność podczas nazywania ich, aby uniknąć kolizji. Jeśli nie używasz makr niepotrzebnie, nie musisz się o to martwić (i nie musisz czytaćSHOUTY_NAMES
).Jeśli chcesz mieć stałą wewnątrz funkcji, nie możesz tego zrobić za pomocą makra, ponieważ preprocesor nie wie, czym jest funkcja ani co to znaczy być w niej. Aby ograniczyć makro tylko do określonej części pliku, musisz to
#undef
zrobić ponownie:int limit(int height) { #define MAX_HEIGHT 720 return std::max(height, MAX_HEIGHT); #undef MAX_HEIGHT }
Porównaj z dużo bardziej rozsądnym:
int limit(int height) { constexpr int max_height = 720; return std::max(height, max_height); }
Dlaczego wolisz makro?
Prawdziwe miejsce w pamięci
Zmienna constexpr jest zmienną, więc faktycznie istnieje w programie i możesz robić normalne rzeczy w C ++, takie jak pobranie jej adresu i powiązanie z nią odwołania.
Ten kod ma niezdefiniowane zachowanie:
#define MAX_HEIGHT 720 int limit(int height) { const int& h = std::max(height, MAX_HEIGHT); // ... return h; }
Problem polega na tym, że
MAX_HEIGHT
nie jest to zmienna, więc wywołanie zmiennejstd::max
tymczasowejint
musi zostać utworzone przez kompilator. Odwołanie, które jest zwracane przezstd::max
może wtedy odnosić się do tego tymczasowego, który nie istnieje po zakończeniu tej instrukcji, więcreturn h
uzyskuje dostęp do nieprawidłowej pamięci.Ten problem po prostu nie istnieje z odpowiednią zmienną, ponieważ ma ona stałą lokalizację w pamięci, która nie znika:
int limit(int height) { constexpr int max_height = 720; const int& h = std::max(height, max_height); // ... return h; }
(W praktyce prawdopodobnie byś tego
int h
nie powiedział,const int& h
ale problem może pojawić się w bardziej subtelnych kontekstach.)Warunki preprocesora
Jedynym momentem, w którym preferujesz makro, jest sytuacja, gdy potrzebujesz zrozumienia jego wartości przez preprocesor, do użycia w
#if
warunkach, np.#define MAX_HEIGHT 720 #if MAX_HEIGHT < 256 using height_type = unsigned char; #else using height_type = unsigned int; #endif
Nie możesz użyć tutaj zmiennej, ponieważ preprocesor nie rozumie, jak odwoływać się do zmiennych według nazwy. Rozumie tylko podstawowe, bardzo podstawowe rzeczy, takie jak rozwijanie makr i dyrektywy zaczynające się od
#
(jak#include
i#define
i#if
).Jeśli potrzebujesz stałej, która może być zrozumiana przez preprocesor , powinieneś użyć preprocesora do jej zdefiniowania. Jeśli chcesz mieć stałą dla normalnego kodu C ++, użyj normalnego kodu C ++.
Powyższy przykład ma na celu jedynie zademonstrowanie warunku preprocesora, ale nawet ten kod mógłby uniknąć użycia preprocesora:
using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
źródło
constexpr
Potrzeba zmienna nie zajmują pamięci aż do jego adres (wskaźnik / odniesienia) jest pobierana; w przeciwnym razie można go całkowicie zoptymalizować (i myślę, że może istnieć Standardese, który to gwarantuje). Chcę to podkreślić, aby ludzie nie kontynuowali używania starego, gorszego „enum
hackowania” z błędnego pomysłu, że trywialnyconstexpr
, który nie wymaga pamięci, mimo wszystko zajmie trochę.int height
byłby to taki sam problem jak makro, ponieważ jego zakres jest powiązany z funkcją, zasadniczo również tymczasowy. 3. Powyższy komentarz „const int & h przedłuży żywotność tymczasowego” jest poprawny.limit
, problemem jest zwracana wartośćstd::max
. 2. tak, dlatego nie zwraca odwołania. 3. źle, zobacz link coliru powyżej.const int& h = max(x, y);
imax
zwraca przez wartość, okres życia wartości zwracanej jest wydłużony. Nie przez typ zwracany, ale przez typ, zconst int&
którym jest powiązany. To co napisałem jest poprawne.Ogólnie rzecz biorąc, powinieneś używać,
constexpr
kiedy tylko możesz, a makr tylko wtedy, gdy żadne inne rozwiązanie nie jest możliwe.Racjonalne uzasadnienie:
Makra są prostym zamiennikiem w kodzie iz tego powodu często generują konflikty (np.
max
Makro windows.h vsstd::max
). Dodatkowo działające makro można łatwo wykorzystać w inny sposób, co może wywołać dziwne błędy kompilacji. (np.Q_PROPERTY
używany na elementach konstrukcji)Z powodu wszystkich tych niepewności dobrym stylem kodu jest unikanie makr, dokładnie tak, jak zwykle unikasz goto.
constexpr
jest zdefiniowana semantycznie i dlatego zazwyczaj generuje znacznie mniej problemów.źródło
#if
rzeczy, do których preprocesor jest faktycznie przydatny. Definiowanie stałej nie jest jedną z rzeczy, do których preprocesor jest przydatny, chyba że ta stała musi być makrem, ponieważ jest używana w warunkach preprocesora przy użyciu#if
. Jeśli stała jest używana w normalnym kodzie C ++ (a nie w dyrektywach preprocesora), użyj normalnej zmiennej C ++, a nie makra preprocesora.Świetna odpowiedź Jonathona Wakely'ego . Radziłbym również zapoznać się z odpowiedzią jogojapan, aby dowiedzieć się, jaka jest różnica między,
const
aconstexpr
nawet zanim zaczniesz rozważać użycie makr.Makra są głupie, ale w dobry sposób. Pozornie w dzisiejszych czasach są one pomocą przy kompilacji, gdy chcesz, aby bardzo konkretne części twojego kodu były kompilowane tylko w obecności określonych parametrów kompilacji, które zostały „zdefiniowane”. Zazwyczaj wszystkie środki, które bierze swoją nazwę makra, albo jeszcze lepiej, nazwijmy to się
Trigger
i dodając rzeczy podoba/D:Trigger
,-DTrigger
itp do narzędzi budowania używane.Chociaż istnieje wiele różnych zastosowań makr, są to dwa, które najczęściej widzę, a które nie są złe / przestarzałe:
Więc chociaż możesz w przypadku OP osiągnąć ten sam cel, jakim jest zdefiniowanie int z
constexpr
lub aMACRO
, jest mało prawdopodobne, że te dwa elementy będą się pokrywać, gdy używasz nowoczesnych konwencji. Oto kilka typowych zastosowań makr, które nie zostały jeszcze wycofane.#if defined VERBOSE || defined DEBUG || defined MSG_ALL // Verbose message-handling code here #endif
Jako kolejny przykład użycia makr, powiedzmy, że masz nadchodzący sprzęt do wydania lub może jego konkretną generację, która ma trudne obejścia, których inne nie wymagają. Zdefiniujemy to makro jako
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
źródło