Onde devo preferir usar macros e onde devo preferir constexpr ? Eles não são basicamente os mesmos?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
Onde devo preferir usar macros e onde devo preferir constexpr ? Eles não são basicamente os mesmos?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
Respostas:
Eles não são basicamente os mesmos?
Não. Absolutamente não. Nem mesmo perto.
Além do fato de que sua macro é uma int
e sua constexpr unsigned
é uma unsigned
, existem diferenças importantes e as macros têm apenas uma vantagem.
Uma macro é definida pelo pré-processador e é simplesmente substituída no código toda vez que ocorre. O pré-processador é burro e não entende a sintaxe ou semântica do C ++. As macros ignoram escopos, como namespaces, classes ou blocos de função, portanto, você não pode usar um nome para qualquer outra coisa em um arquivo de origem. Isso não é verdade para uma constante definida como uma variável C ++ adequada:
#define MAX_HEIGHT 720
constexpr int max_height = 720;
class Window {
// ...
int max_height;
};
É bom ter uma variável de membro chamada max_height
porque é um membro de classe e, portanto, tem um escopo diferente e é diferente daquele no escopo do namespace. Se você tentar reutilizar o nome MAX_HEIGHT
do membro, o pré-processador irá alterá-lo para este absurdo que não compilaria:
class Window {
// ...
int 720;
};
É por isso que você deve fornecer macros UGLY_SHOUTY_NAMES
para garantir que eles se destaquem e você pode ter cuidado ao nomeá-los para evitar conflitos. Se você não usa macros desnecessariamente, não precisa se preocupar com isso (e não precisa ler SHOUTY_NAMES
).
Se você quer apenas uma constante dentro de uma função, você não pode fazer isso com uma macro, porque o pré-processador não sabe o que é uma função ou o que significa estar dentro dela. Para limitar uma macro a apenas uma determinada parte de um arquivo, você precisa #undef
dela novamente:
int limit(int height) {
#define MAX_HEIGHT 720
return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}
Compare com o muito mais sensato:
int limit(int height) {
constexpr int max_height = 720;
return std::max(height, max_height);
}
Por que você prefere o macro?
Uma variável constexpr é uma variável, então ela realmente existe no programa e você pode fazer coisas normais em C ++ como pegar seu endereço e vincular uma referência a ele.
Este código tem comportamento indefinido:
#define MAX_HEIGHT 720
int limit(int height) {
const int& h = std::max(height, MAX_HEIGHT);
// ...
return h;
}
O problema é que MAX_HEIGHT
não é uma variável, portanto, para a chamada de std::max
um temporário int
deve ser criada pelo compilador. A referência que é retornada por std::max
pode então referir-se àquele temporário, que não existe após o final dessa instrução, então return h
acessa a memória inválida.
Esse problema simplesmente não existe com uma variável adequada, porque tem um local fixo na memória que não desaparece:
int limit(int height) {
constexpr int max_height = 720;
const int& h = std::max(height, max_height);
// ...
return h;
}
(Na prática, você provavelmente int h
não declararia, const int& h
mas o problema pode surgir em contextos mais sutis.)
O único momento para preferir uma macro é quando você precisa que seu valor seja compreendido pelo pré-processador, para uso em #if
condições, por exemplo
#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif
Você não pode usar uma variável aqui, porque o pré-processador não entende como se referir às variáveis pelo nome. Ele só entende coisas básicas muito básicas, como expansão macro e diretivas que começam com #
(como #include
e #define
e #if
).
Se você quiser uma constante que possa ser entendida pelo pré - processador , você deve usar o pré-processador para defini-la. Se você quiser uma constante para o código C ++ normal, use o código C ++ normal.
O exemplo acima é apenas para demonstrar uma condição de pré-processador, mas mesmo esse código pode evitar o uso do pré-processador:
using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
constexpr
variável não precisa ocupar memória até que seu endereço (um ponteiro / referência) seja obtido; caso contrário, pode ser totalmente otimizado (e acho que pode haver o Standardese que garante isso). Quero enfatizar isso para que as pessoas não continuem usando o velho e inferior ' enum
hack' de uma ideia equivocada de que um trivial constexpr
que não requer armazenamento irá ocupar algum.
int height
seria tão problemático quanto a macro, já que seu escopo está vinculado à função, essencialmente temporário. 3. O comentário acima, "const int & h irá estender a vida útil do temporário" está correto.
limit
, o problema é o valor de retorno de std::max
. 2. sim, por isso não retorna uma referência. 3. errado, veja o link coliru acima.
const int& h = max(x, y);
e max
retornar pelo valor, a vida útil do valor de retorno será estendida. Não pelo tipo de retorno, mas pelo ao const int&
qual está vinculado. O que escrevi está correto.
De modo geral, você deve usar constexpr
sempre que puder e macros apenas se nenhuma outra solução for possível.
As macros são uma simples substituição no código e, por esse motivo, costumam gerar conflitos (por exemplo, max
macro windows.h vs std::max
). Além disso, uma macro que funciona pode ser facilmente usada de uma maneira diferente, o que pode desencadear erros de compilação estranhos. (por exemplo, Q_PROPERTY
usado em membros da estrutura)
Devido a todas essas incertezas, é um bom estilo de código evitar macros, exatamente como você normalmente evitaria gotos.
constexpr
é semanticamente definido e, portanto, normalmente gera muito menos problemas.
#if
coisas para as quais o pré-processador é realmente útil. Definir uma constante não é uma das coisas para as quais o pré-processador seja útil, a menos que essa constante deva ser uma macro porque é usada em condições de pré-processador #if
. Se a constante for para uso em código C ++ normal (não em diretivas de pré-processador), use uma variável C ++ normal, não uma macro de pré-processador.
Ótima resposta de Jonathon Wakely . Eu também aconselho você a dar uma olhada na resposta de Jogojapan sobre qual é a diferença entre const
e constexpr
antes mesmo de considerar o uso de macros.
As macros são burras, mas no bom sentido. Aparentemente, hoje em dia, eles são um auxílio de construção para quando você deseja que partes muito específicas do seu código sejam compiladas apenas na presença de certos parâmetros de construção sendo "definidos". Geralmente, tudo o que meios está tomando seu nome macro, ou melhor ainda, vamos chamá-lo um Trigger
, e as coisas adicionando gosta, /D:Trigger
, -DTrigger
, etc, para as ferramentas de compilação que está sendo usado.
Embora haja muitos usos diferentes para macros, estes são os dois que vejo com mais frequência que não são práticas ruins / desatualizadas:
Portanto, embora você possa, no caso do OP, atingir o mesmo objetivo de definir um int com constexpr
ou a MACRO
, é improvável que os dois se sobreponham ao usar as convenções modernas. Aqui estão alguns usos de macro comuns que ainda não foram eliminados.
#if defined VERBOSE || defined DEBUG || defined MSG_ALL
// Verbose message-handling code here
#endif
Como outro exemplo de uso de macro, digamos que você tenha algum hardware para lançar, ou talvez uma geração específica dele que tenha algumas soluções alternativas complicadas que os outros não requerem. Vamos definir essa macro como 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