ti-enxame.com

Constexpr vs macros

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;
54
Tom Dorone

Eles não são basicamente os mesmos?

Não. Absolutamente não. Nem mesmo perto.

Além do fato de sua macro ser um int e seu constexpr unsigned ser um unsigned, existem diferenças importantes e as macros têm apenas ma 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 a semântica do C++. As macros ignoram escopos como namespaces, classes ou blocos de funções, portanto, você não pode usar um nome para mais nada 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 da classe e, portanto, tem um escopo diferente, e é distinto daquele no escopo do espaço para nome. Se você tentasse reutilizar o nome MAX_HEIGHT para o membro, o pré-processador o alteraria para esse absurdo que não seria compilado:

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

É por isso que você precisa fornecer macros UGLY_SHOUTY_NAMES para garantir que elas se destaquem e você pode ter cuidado ao nomeá-las 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, 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 parte de um arquivo, você precisa #undef 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 a macro?

Um local de memória real

Uma variável constexpr é uma variável , portanto ela 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 um 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 para std::max, um int temporário deve ser criado pelo compilador. A referência retornada por std::max pode se referir a esse temporário, que não existe após o final dessa instrução, para que return h acesse a memória inválida.

Esse problema simplesmente não existe com uma variável adequada, porque possui 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 declararia int h não const int& h, mas 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 entendido pelo pré-processador, para uso em condições #if, 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 de macro e diretivas começando com # (como #include e #define e #if).

Se você deseja uma constante que possa ser entendida pelo pré-processador , use o pré-processador para defini-la. Se você deseja 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 poderia evitar o uso do pré-processador:

using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
102
Jonathan Wakely

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

Fundamentação da petição:

As macros são uma substituição simples no código e, por esse motivo, geralmente geram conflitos (por exemplo, windows.h max macro vs std::max). Além disso, uma macro que funciona pode ser facilmente usada de uma maneira diferente, que pode desencadear erros estranhos de compilação. (por exemplo, Q_PROPERTY usado em membros da estrutura)

Devido a todas essas incertezas, é bom estilo de código evitar macros, exatamente como você costuma evitar gotos.

constexpr é definido semanticamente e, portanto, normalmente gera muito menos problemas.

6
Adrian Maire

Ótima resposta por Jonathon Wakely . Também recomendo que você dê uma olhada em resposta do jogojapan sobre a diferença entre const e constexpr antes mesmo de considerar o uso de macros.

As macros são burras, mas de uma maneira boa . Ostensivelmente hoje em dia eles são um auxiliar de construção para quando você deseja que partes muito específicas do seu código sejam compiladas apenas na presença de determinados parâmetros de construção que são "definidos". Geralmente, tudo o que isso significa é pegar o nome da sua macro ou, melhor ainda, vamos chamá-la de Trigger e adicionar coisas como /D:Trigger, -DTrigger, etc. às ferramentas de construção que estão sendo usadas .

Embora haja muitos usos diferentes para macros, esses são os dois que vejo com mais freqüência que não são práticas ruins/desatualizadas:

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

Portanto, embora você possa, no caso do OP, atingir o mesmo objetivo de definir um int com constexpr ou um MACRO, é improvável que os dois se sobreponham ao usar convenções modernas. Aqui estão alguns usos comuns de macro 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 a ser lançado, ou talvez uma geração específica dele que tenha algumas soluções complicadas que os outros não exigem. 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
2
kayleeFrye_onDeck