Constexpr a makra

92

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;
Tom Dorone
źródło
4
AFAIK constexpr zapewnia większe bezpieczeństwo typów
Code-Apprentice
13
Łatwe: constexr, zawsze.
n. zaimki m.
Może odpowiedzieć na niektóre z Twoich pytań stackoverflow.com/q/4748083/540286
Ortwin Angermeier

Odpowiedzi:

146

Czy nie są w zasadzie takie same?

Nie, absolutnie nie. Nawet nie blisko.

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 ++:

#define MAX_HEIGHT 720
constexpr int max_height = 720;

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

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:

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

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:

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_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:

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 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.

#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 #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, unsigned char, unsigned int>;
Jonathan Wakely
źródło
3
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.

Adrian Maire
źródło
1
W jakim przypadku użycie makra jest nieuniknione?
Tom Dorone,
3
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:

  1. Sekcje kodu specyficzne dla sprzętu i platformy
  2. 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
kayleeFrye_onDeck
źródło