Constexpr vs macros


Respostas:


151

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 inte sua constexpr unsignedé uma unsigned, existem diferenças importantes e as macros têm apenas uma vantagem.

Escopo

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_heightporque é um membro de classe e, portanto, tem um escopo diferente e é diferente daquele no escopo do namespace. Se você tentar reutilizar o nome MAX_HEIGHTdo 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_NAMESpara 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 #undefdela 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?

Um local de memória real

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_HEIGHTnão é uma variável, portanto, para a chamada de std::maxum temporário intdeve ser criada pelo compilador. A referência que é retornada por std::maxpode então referir-se àquele temporário, que não existe após o final dessa instrução, então return hacessa 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 hnão declararia, const int& hmas o problema pode surgir em contextos mais sutis.)

Condições do pré-processador

O único momento para preferir uma macro é quando você precisa que seu valor seja compreendido pelo pré-processador, para uso em #ifcondiçõ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 #includee #definee #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>;

3
Uma constexprvariá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 ' enumhack' de uma ideia equivocada de que um trivial constexprque não requer armazenamento irá ocupar algum.
sublinhado_d

3
Sua seção "A real memory location" está errada: 1. Você está retornando por valor (int), então uma cópia é feita, o temporário não é um problema. 2. Se você tivesse retornado por referência (int &), int heightseria 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.
PoweredByRice

4
@underscore_d true, mas isso não muda o argumento. A variável não exigirá armazenamento a menos que haja um uso odr dela. O ponto é que, quando uma variável real com armazenamento é necessária, a variável constexpr faz a coisa certa.
Jonathan Wakely

1
@PoweredByRice 1. o problema não tem nada a ver com o valor de retorno de 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.
Jonathan Wakely

3
@PoweredByRice suspiro, você realmente não precisa explicar como o C ++ funciona para mim. Se você tiver const int& h = max(x, y);e maxretornar 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.
Jonathan Wakely

11

De modo geral, você deve usar constexprsempre que puder e macros apenas se nenhuma outra solução for possível.

Justificativa:

As macros são uma simples substituição no código e, por esse motivo, costumam gerar conflitos (por exemplo, maxmacro 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_PROPERTYusado 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.


1
Em que caso o uso de uma macro é inevitável?
Tom Dorone

3
Compilação condicional usando, por exemplo, #ifcoisas 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.
Jonathan Wakely

Exceto usando macros variadic, principalmente o uso de macro para chaves de compilador, mas tentar substituir as instruções de macro atuais (como condicionais, chaves de string literal) lidando com instruções de código reais com constexpr é uma boa ideia?

Eu diria que as opções do compilador também não são uma boa ideia. No entanto, eu entendo perfeitamente que é necessário algumas vezes (também macros), especialmente quando se trata de plataforma cruzada ou código incorporado. Para responder à sua pergunta: Se você já está lidando com pré-processador, eu usaria macros para manter claro e intuitivo o que é pré-processador e o que é tempo de compilação. Eu também sugeriria comentar bastante e torná-lo o mais curto e local possível (evite macros espalhadas ou 100 linhas #if). Talvez a exceção seja o guarda #ifndef típico (padrão para # pragma uma vez) que é bem compreendido.
Adrian Maire,

3

Ótima resposta de Jonathon Wakely . Eu também aconselho você a dar uma olhada na resposta de Jogojapan sobre qual é a diferença entre conste constexprantes 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:

  1. Seções de código específicas de hardware e plataforma
  2. Maior verbosidade aumenta

Portanto, embora você possa, no caso do OP, atingir o mesmo objetivo de definir um int com constexprou 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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.