Gdzie powinienem preferować używanie makr, a gdzie powinienem preferować constexpr ? Czy nie są w zasadzie takie same?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
Gdzie powinienem preferować używanie makr, a gdzie powinienem preferować constexpr ? Czy nie są w zasadzie takie same?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
Odpowiedzi:
Czy nie są w zasadzie takie same?
Nie, absolutnie nie. Nawet nie blisko.
Pomijając fakt, że Twoje makro to, int
a Twoje constexpr unsigned
jest unsigned
, istnieją istotne różnice, a makra mają tylko jedną zaletę.
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ć nazwy MAX_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?
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 zmiennej std::max
tymczasowej int
musi zostać utworzone przez kompilator. Odwołanie, które jest zwracane przez std::max
może wtedy odnosić się do tego tymczasowego, który nie istnieje po zakończeniu tej instrukcji, więc return 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.)
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>;
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 trywialny constexpr
, 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);
i max
zwraca 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.
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.
Makra są prostym zamiennikiem w kodzie iz tego powodu często generują konflikty (np. max
Makro 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_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.
#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
a constexpr
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 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