Constexpr vs 宏

我应该在哪里更喜欢使用 和我应该在哪里更喜欢 Constexpr? 他们基本上不是一样的吗?

#define MAX_HEIGHT 720

constexpr unsigned int max_height = 720;
63874 次浏览

一般来说,只要有可能就应该使用 constexpr,只有在没有其他解决方案的情况下才使用宏。

理由:

宏是代码中的一个简单替代品,因此,它们常常会产生冲突(例如,windows.h max宏与 std::max)。此外,一个可以工作的宏可以很容易地以不同的方式使用,然后可以触发奇怪的编译错误。(例如,用于结构成员的 Q_PROPERTY)

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

constexpr是语义定义的,因此通常产生的问题要少得多。

他们基本上不是一样的吗?

不,绝对不行,差远了。

除了你的宏是一个 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的调用,编译器必须创建一个临时 int。然后,std::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>;

乔纳森 · 韦克利的回答很棒。我还建议您在考虑宏的使用之前先看一下 Jogojapan 的回答,了解一下 constconstexpr之间的区别。

宏是愚蠢的,但以 很好的方式。表面上看,现在它们是一种构建辅助工具,当您希望只在某些构建参数被“定义”的情况下编译代码的非常特定的部分时,可以使用它们进行编译。通常,所有这些意味着使用您的宏名称,或者更好的方法,我们称之为 Trigger,然后将诸如 /D:Trigger-DTrigger等添加到正在使用的构建工具中。

虽然宏有许多不同的用途,但是我经常看到的两种用法并不过时:

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

因此,尽管在 OP 中可以用 constexprMACRO实现同样的目标,但是在使用现代约定时,这两者不太可能有重叠。下面是一些尚未淘汰的常见宏用法。

#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