Constexpr与宏的区别

139

我应该在哪里使用,在哪里使用constexpr?它们不是基本上一样吗?

#define MAX_HEIGHT 720

对抗

constexpr unsigned int max_height = 720;

6
据我所知,constexpr 提供更多的类型安全性。 - Code-Apprentice
25
简单易用:constexr,永远如此。 - n. m.
可能会回答你的一些问题 https://dev59.com/zW445IYBdhLWcg3wq8FY - Ortwin Angermeier
如果宏定义类似于 #define LOG if(logger) loggger->log(),我还可以使用 constexpr 吗? - KcFnMi
@KcFnMi 不是的,这就是函数的作用。void log() { if(logger != nullptr) logger->log(); } - Pharap
3个回答

213
它们基本上不一样吗? 绝对不是。差别很大。 除了你的宏是一个int类型,并且你的constexpr unsigned是一个unsigned类型之外,还有重要的区别,宏只有一个优点。
范围 宏由预处理器定义,并且每次出现时都会被简单地替换到代码中。预处理器是愚蠢的,不理解C++的语法或语义。宏忽略命名空间、类或函数块等作用域,因此在源文件中不能将名称用于其他任何内容。而对于作为合适的C++变量定义的常数来说,情况并非如此。
#define MAX_HEIGHT 720
constexpr int max_height = 720;

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

在类成员中使用名为max_height的数据成员是可以的,因为它具有不同的作用域,并且与命名空间作用域中的变量是不同的。如果您尝试重用成员的名称MAX_HEIGHT,预处理器会将其更改为无法编译的无意义内容:

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

这就是为什么你必须给宏起一个“UGLY_SHOUTY_NAMES”的名字,以确保它们能够突出显示,并且你可以小心地命名它们以避免冲突。如果你不必要地使用宏,就不必担心这个问题(也不必阅读“SHOUTY_NAMES”)。
如果你只想在函数内部使用一个常量,宏就无法做到这一点,因为预处理器不知道函数是什么,也不知道在函数内部的含义。要将宏限制在文件的某个特定部分,你需要再次使用“#undef”取消定义它:
int limit(int height) {
#define MAX_HEIGHT 720
  return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}

相比之下,更明智的是:
int limit(int height) {
  constexpr int max_height = 720;
  return std::max(height, max_height);
}

为什么你更喜欢宏定义的那个?
一个真实的内存位置
constexpr变量是一个变量,所以它实际上存在于程序中,你可以像普通的C++变量一样获取它的地址并绑定引用。
以下代码具有未定义行为:
#define MAX_HEIGHT 720
int limit(int height) {
  const int& h = std::max(height, MAX_HEIGHT);
  // ...
  return h;
}

问题在于MAX_HEIGHT不是一个变量,所以对于调用std::max,编译器必须创建一个临时的intstd::max返回的引用可能指向那个临时变量,在该语句结束后就不存在了,所以return h访问了无效的内存。
使用正确的变量就不存在这个问题,因为它在内存中有一个固定的位置,不会消失:
int limit(int height) {
  constexpr int max_height = 720;
  const int& h = std::max(height, max_height);
  // ...
  return h;
}

(在实践中,您可能会声明 int h 而不是 const int& h,但问题可能会在更微妙的上下文中出现。)

预处理条件

只有当您需要预处理器理解宏的值以供 #if 条件使用时,才应优先选择宏,例如:

#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif

在这里你不能使用变量,因为预处理器不知道如何通过名称引用变量。它只能理解基本的东西,比如宏展开和以#开头的指令(比如#include#define#if)。

如果你想要一个可以被预处理器理解的常量,那么你应该使用预处理器来定义它。如果你想要一个用于普通C++代码的常量,就使用普通的C++代码。

上面的例子只是为了演示预处理器条件,但即使那段代码也可以避免使用预处理器:

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

2
@TobySpeight 不对,两个都错了。你不能将 int& 绑定到结果上,因为它返回的是 const int&,所以编译不会通过。而且它也不会延长生命周期,因为你没有直接将引用绑定到临时对象上。请参见 http://coliru.stacked-crooked.com/a/873862de9cd8c175 - Jonathan Wakely
4
一个 constexpr 变量在其地址(指针/引用)被使用之前不需要占用内存;否则,它可以完全被优化掉(我认为可能有某些标准规定保证这一点)。我想强调这一点,以便人们不会继续使用旧的、低效的“枚举 hack”,因为他们错误地认为一个不需要存储的平凡 constexpr 仍然会占用一些内存。 - underscore_d
5
你的 "一个真实的内存位置" 部分是错误的:
  1. 你返回的是值(int),所以会复制一份,这个临时变量并不是问题。
  2. 如果你返回引用(int&),那么你的 int height 与宏定义一样也会成为问题,因为它的作用域与函数绑定,本质上也是临时的。
  3. 上面的评论,“const int& h将延长临时变量的生命周期”是正确的。
- PoweredByRice
6
@underscore_d 是正确的,但这并不改变论点。只有在对变量进行odr-use时,才需要存储该变量。关键是当需要具有存储空间的实际变量时,constexpr变量会执行正确的操作。 - Jonathan Wakely
5
@PoweredByRice 叹气,你真的不需要向我解释C++是如何工作的。如果你有const int& h = max(x, y);并且max通过值返回,那么它返回值的生存期会被延长。这种延长不是由返回类型决定的,而是由绑定返回值的const int&决定的。我写的是正确的。 - Jonathan Wakely
显示剩余12条评论

20

一般来说,尽可能使用 constexpr ,仅当没有其他解决方案时才使用宏。

原因:

宏在代码中是简单的替换,因此经常会产生冲突(例如,windows.h 的 max 宏与 std::max )。此外,可行的宏很容易以不同的方式使用,从而触发奇怪的编译错误(例如,在结构成员上使用 Q_PROPERTY )。

由于所有这些不确定性,好的代码风格是避免使用宏,就像通常避免使用 goto 一样。

constexpr 是语义定义的,因此通常会生成更少的问题。


1
在什么情况下使用宏是不可避免的? - Tom Dorone
6
条件编译使用 #if,也就是预处理器实际有用的东西。定义常量不是预处理器有用的东西之一,除非该常量必须是宏,因为它在预处理条件中使用了 #if。如果该常量用于普通的 C++ 代码(而不是预处理器指令),则应使用普通的 C++ 变量,而不是预处理器宏。 - Jonathan Wakely
除了使用可变参数宏之外,大多数宏用于编译器开关,但是尝试使用constexpr替换当前的宏语句(如条件语句、字符串文字开关)来处理实际代码语句是一个好主意吗? - user6952310
我认为编译器开关也不是一个好主意。然而,我完全理解有时候需要使用它们(还有宏),特别是处理跨平台或嵌入式代码。回答你的问题:如果你已经在处理预处理器,我会使用宏来清晰和直观地区分预处理器和编译时间。我还建议大量注释,并尽可能将其用法保持简短和局部化(避免宏扩散或100行#if)。也许例外是典型的#ifndef保护(标准为#pragma once),这是众所周知的。 - Adrian Maire

11

Jonathon Wakely给出了很好的答案。在考虑使用宏之前,我还建议您查看jogojapan的回答,了解constconstexpr之间的区别。

宏是愚蠢的,但以一种好的方式。现在,它们基本上是构建辅助工具,用于当您希望代码的特定部分仅在某些构建参数被“定义”时编译时。通常,这意味着获取您的宏名称,或者更好的是,让我们称其为触发器,并将类似于/D:Trigger-DTrigger等内容添加到正在使用的构建工具中。

虽然有许多不同的宏用途,但以下是我最常见到的两个不是坏的/过时的做法:

  1. 硬件和平台特定代码部分
  2. 增加冗长的构建

因此,在OP的情况下,尽管您可以使用constexprMACRO来实现相同的定义int目标,但在使用现代约定时,两者不太可能重叠。以下是一些常见的尚未淘汰的宏用法。

#if defined VERBOSE || defined DEBUG || defined MSG_ALL
    // Verbose message-handling code here
#endif

作为宏用法的另一个例子,假设您有一些即将发布的硬件,或者可能是某个特定的代数,需要一些棘手的解决方法,而其他代数不需要。我们将定义这个宏为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

第二个选项是不好的做法,应该用调用返回宏值的constexpr函数的if constexpr来替换。原因是你仍然希望编译器测试你的更改不会在除了你正在工作的平台之外的其他平台上破坏编译。当然,例外情况是当你使用的包含和函数在不同平台上完全不同,而且你根本没有相同的函数可供调用时。 - Len

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接