我应该在哪里更喜欢使用 宏和我应该在哪里更喜欢 Constexpr? 他们基本上不是一样的吗?
#define MAX_HEIGHT 720
对
constexpr unsigned int max_height = 720;
一般来说,只要有可能就应该使用 constexpr,只有在没有其他解决方案的情况下才使用宏。
constexpr
宏是代码中的一个简单替代品,因此,它们常常会产生冲突(例如,windows.h max宏与 std::max)。此外,一个可以工作的宏可以很容易地以不同的方式使用,然后可以触发奇怪的编译错误。(例如,用于结构成员的 Q_PROPERTY)
max
std::max
Q_PROPERTY
由于所有这些不确定性,避免使用宏是一种很好的代码风格,就像您通常避免使用 gotos 一样。
constexpr是语义定义的,因此通常产生的问题要少得多。
他们基本上不是一样的吗?
不,绝对不行,差远了。
除了你的宏是一个 int和你的 constexpr unsigned是一个 unsigned的事实,还有重要的差异和宏只有 一的优势。
int
constexpr unsigned
unsigned
宏由预处理器定义,并且每次出现时都被简单地替换到代码中。预处理器是 笨蛋,不懂 C + + 语法或语义。宏忽略命名空间、类或函数块等作用域,因此不能对源文件中的任何其他内容使用名称。对于定义为正确的 C + + 变量的常量来说,情况并非如此:
#define MAX_HEIGHT 720 constexpr int max_height = 720; class Window { // ... int max_height; };
拥有一个名为 max_height的成员变量是可以的,因为它是一个类成员,因此具有不同的作用域,并且与名称空间作用域中的成员变量不同。如果你试图为成员重用 MAX_HEIGHT这个名字,那么预处理器会把它改成这个不能编译的无意义的名字:
max_height
MAX_HEIGHT
class Window { // ... int 720; };
这就是为什么你必须给宏 UGLY_SHOUTY_NAMES,以确保他们脱颖而出,你可以注意命名他们,以避免冲突。如果没有不必要地使用宏,就不必担心这个问题(也不必读取 SHOUTY_NAMES)。
UGLY_SHOUTY_NAMES
SHOUTY_NAMES
如果你只想在函数里面有一个常量,你不能用宏来实现,因为预处理器不知道函数是什么,也不知道在函数里面意味着什么。要将宏限制在文件的某个特定部分,需要再次对其进行 #undef:
#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访问无效内存。
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,但问题可能出现在更微妙的上下文中。)
int h
const int& h
选择宏的唯一时机是当您需要预处理器理解它的值时,以便在 #if条件下使用,例如。
#if
#define MAX_HEIGHT 720 #if MAX_HEIGHT < 256 using height_type = unsigned char; #else using height_type = unsigned int; #endif
这里不能使用变量,因为预处理器不知道如何按名称引用变量。它只理解基本的非常基本的东西,如宏扩展和指令开始与 #(如 #include和 #define和 #if)。
#
#include
#define
如果您想要一个常量 可以被预处理器理解,那么您应该使用预处理器来定义它。如果您想为普通的 C + + 代码设置一个常量,那么可以使用普通的 C + + 代码。
上面的例子只是演示一个预处理器条件,但即使是这样的代码也可以避免使用预处理器:
using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
乔纳森 · 韦克利的回答很棒。我还建议您在考虑宏的使用之前先看一下 Jogojapan 的回答,了解一下 const和 constexpr之间的区别。
const
宏是愚蠢的,但以 很好的方式。表面上看,现在它们是一种构建辅助工具,当您希望只在某些构建参数被“定义”的情况下编译代码的非常特定的部分时,可以使用它们进行编译。通常,所有这些意味着使用您的宏名称,或者更好的方法,我们称之为 Trigger,然后将诸如 /D:Trigger、 -DTrigger等添加到正在使用的构建工具中。
Trigger
/D:Trigger
-DTrigger
虽然宏有许多不同的用途,但是我经常看到的两种用法并不过时:
因此,尽管在 OP 中可以用 constexpr或 MACRO实现同样的目标,但是在使用现代约定时,这两者不太可能有重叠。下面是一些尚未淘汰的常见宏用法。
MACRO
#if defined VERBOSE || defined DEBUG || defined MSG_ALL // Verbose message-handling code here #endif
作为宏使用的另一个例子,假设您有一些即将发布的硬件,或者可能有一些其他硬件不需要的棘手的变通方法的特定一代硬件。我们将这个宏定义为 GEN_3_HW。
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