狂野西部编程:宏






4.64/5 (3投票s)
标准宏的一种替代解决方案,旨在让宏“少一点邪恶”。
引言
宏基于#define
指令,该指令指定一个宏,它包含一个标识符名称和一个要由预处理器替换源代码中该宏的每个实例的字符串
或数值。宏通常用于抽象 pragma、declspecs、属性、先决条件和功能检查。
许多程序员相信知道宏是邪恶的。这方面的一个主要原因是它们会污染代码库,因为它们会覆盖任何同名的内容。尽管如此,它们通常是跨平台访问编译器特定功能的唯一方法。宏在两个事物之间需要小心权衡。第一个是唯一命名。这会导致代码库污染,因为宏是全局的。它们通常在第一次使用前定义,在最后一次使用后立即取消定义。这有助于抵消污染。第二个考虑是使名称有意义。这两者的结合通常会导致结果不尽如人意。作为替代,我们将提供一个使用属性式命名(attribute-style naming)的可移植性包装器,以解决此冲突。
背景
在处理 CppCon 视频中提出的一个代码库挑战时,我想到了一些关于编程中一些更具争议性主题的替代方案。这些最初纯粹是学术性的想法,似乎具有广泛的实际用途。尽管其中一些替代方案需要库结构来支持,这些库结构也需要解释。一个想法开始形成。一系列文章,旨在解决编码中的争议性概念并提供替代方法和手段。其中第一个将是最基础的。宏。
设计
我称之为“特性”(characteristic)的基本设计是一个宏,它本质上是四个独立的宏协同工作。第一个是分派 ID(dispatch id)。ID 的主要功能是拥有一个长而唯一的名称,发生冲突的可能性很小或不大。这也是构建宏功能的位置。让我们看一个可能的 ID 外观示例。
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
# if defined(__clang__)
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline \
__attribute__((always_inline, flatten, hot)) inline
# elif defined(__GNUC__) || defined(__GNUG__)
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline \
__attribute__((always_inline, flatten, optimize("-O3"))) inline
# elif (defined(_MSC_VER) && defined(_MSVC_LANG))
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline \
__pragma(auto_inline(on))__pragma(inline_recursion(on)) __forceinline
# else
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline inline
# endif
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
此宏定义了一组属性、pragma 和关键字的组合,以构成一个强制内联宏。此宏将以 Msvc、Gcc 和 Clang 为目标。它在给定的编译器上效果惊人,尽管作者认为关键字 explicit 应该用作 inline 的修饰符,以及其他内容,以便在语言本身中实现此功能。但我跑题了,所有的宏都采用大写命名,除了结尾的名称。请注意这一点,因为稍后会对此进行更多讨论。
下一个宏是转发宏(forwarding macro)。它的作用正如其名。它转发给定的标记。在此宏中,给定的标记将与前缀 CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_
连接,该前缀将展开为我们对应的 ID。
# define CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__( __DISPATCH_ID__, ... ) \
CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_ ## \__DISPATCH_ID__
__VA_ARGS__ __DISPATCH_ARGS__EMPTY__
下一个宏是分派宏本身(dispatch macro)。它调用转发宏。通过此调用添加第二组括号,完成对转发器的调用。这为我们提供了属性风格的语法,同时确保在各种编译器上实现所需的展开。
# define CHARACTERISTIC_DISPATCHER( ... ) \
CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__ __VA_ARGS__ __DISPATCH_ARGS__EMPTY__
最后一个宏是访问宏(access macro),它的唯一目的是取消定义。由于 ID 名称很长,不太可能引起命名冲突。相比之下,访问宏应该很短,并反映其所使用的项目或库的名称。此访问宏可以根据需要取消定义和重新定义。请注意,这是用于演示目的,因为应使用更合适、更短的库名称。
#ifndef __characteristic__
// characteristic : dispatch macro - abstract pragmas,
// declspecs, attributes, prerequisites, and feature checks.
// Uses names long enough to discourage name pollution while keeping the entries meaningful.
# define __characteristic__( ... ) CHARACTERISTIC_DISPATCHER( __VA_ARGS__ )
#endif // __characteristic__
现在我们有了一个可以在代码中轻松定义和取消定义的宏,而长名称得以保留。为了增加措施,这里有一个检查 constexpr
可用性的 ID,如果找到,则进行强制内联 constexpr
,否则仅回退到强制内联方案。
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
#if defined(__cpp_constexpr) && __cpp_constexpr >= 201304
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
# CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline constexpr
#else
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
# CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
#endif
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
分派宏可以压缩成一个宏。标识符是最后一个下划线后的最后一个名称。这些应该是小写的,因为用作标识符的宏将在完整的 istribution ID 连接之前展开。这可能是期望的效果。但大多数情况下不是,因此建议使用小写的 ID 结尾,以免意外的宏替换特性标识符。因此,选择了属性语法。
Using the Code
尽管这是一个牵强的例子,可能可以做得更简单,但核心思想是特性展开为一个更长、更独特的宏,不会污染代码库。这有助于保持代码的可读性,而无需担心名称会重复出现。而更短、更易读的名称可以被取消定义。更合适的用法是装饰 std::invoke
的结构和函数,以实现零开销调用。这会将函数调用保留在其位置。也就是说,这里有一个使用我们激进优化的简单阶乘。
//__characteristic__((inline))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
# if defined(__clang__)
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
# __attribute__((always_inline, flatten, hot)) inline
# elif defined(__GNUC__) || defined(__GNUG__)
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
# __attribute__((always_inline, flatten, optimize("-O3"))) inline
# elif (defined(_MSC_VER) && defined(_MSVC_LANG))
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
# __pragma(auto_inline(on))__pragma(inline_recursion(on)) __forceinline
# else
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline inline
# endif
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
//__characteristic__((constexpr))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
#if defined(__cpp_constexpr) && __cpp_constexpr >= 201304
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
# CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline constexpr
#else
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
# CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
#endif
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
#ifndef __characteristic__
# define CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__( __DISPATCH_ID__, ... )
# CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_ ##
# __DISPATCH_ID__ __VA_ARGS__ __DISPATCH_ARGS__EMPTY__
# define __DISPATCH_ARGS__EMPTY__
# define CHARACTERISTIC_DISPATCHER_FUNCTION_MACRO( ... )
# CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__
# __VA_ARGS__ __DISPATCH_ARGS__EMPTY__
/*
characteristic : dispatch macro - abstract pragmas,
declspecs, attributes, prerequisites, and feature checks.
Uses names long enough to discourage name pollution while keeping the entries meaningful.
*/
# define __characteristic__( ... ) CHARACTERISTIC_DISPATCHER_FUNCTION_MACRO( __VA_ARGS__ )
#endif // __characteristic__
#include <type_traits>
#include <iostream>
#include <stdexcept>
// force inline constexpr, or force inline
__characteristic__((constexpr)) // ids must be lower case to prevent unwanted expansions
int factorial(int n)
{
return n <= 1 ? 1 : (n * factorial(n - 1));
}
// output function that requires a compile-time constant, for testing
template<int n>
struct ConstNOut
{
__characteristic__((inline)) ConstNOut() { std::cout << n << '\n'; }
};
int main()
{
std::cout << "4! = ";
ConstNOut<factorial(4)> out1; // computed at compile time via constexpr
volatile int k = 8; // falls back to force inline
std::cout << k << "! = " << factorial(k) << '\n'; // inlined
}
#undef __characteristic__
即使这些是保留关键字,它们也会展开为我们对应的 ID。因此,它们不会覆盖任何内容,同时具有有意义的名称。这里,包装的 inline 表示强制 inline。包装的 constexpr
表示强制 inline constexpr
。而任一未包装的形式都没有强制内联。构建 noinline、nodiscard 等常见 ID 的宏是微不足道的。此外,关键字具有额外的语法高亮显示,属性说明符(attribute specifiers)具有对应的类型名称。再次强调,关键字、类和变量名是首选,因为它们不太可能被其他宏影响。这就是为什么在标识符的结尾部分使用小写字母,因为小写字母不太可能是另一个宏。但它们不限于单个标识符。它们可以支持带有参数的函数式宏。例如...
//__characteristic__((error("output this error...")))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error
# if defined(_MSC_VER) && !defined( __GNUC__ ) &&
# !defined( __GNUG__ ) && !defined( __clang__ )
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error( MESSAGE )
# __pragma(message(": error: " \
MESSAGE))
# else // _MSC_VER
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error( MESSAGE )
# _Pragma( CHARACTERISTIC_STRINGIZE
# ( GCC error(CHARACTERISTIC_EXPAND__(MESSAGE)) ) )
# endif //_MSC_VER
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error
//__characteristic__((warning("output this warning...")))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_warning
# if defined(_MSC_VER) && !defined( __GNUC__ ) &&
# !defined( __GNUG__ ) && !defined( __clang__ )
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_warning
# ( MESSAGE ) __pragma(message(": warning: " \
MESSAGE))
# else // _MSC_VER
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_warning( MESSAGE )
# _Pragma( CHARACTERISTIC_STRINGIZE
# ( GCC warning(CHARACTERISTIC_EXPAND__(MESSAGE)) ) )
# endif //_MSC_VER
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_message
以及它依赖的字符串化...
#ifndef CHARACTERISTIC_STRINGIZE
/* basic stringize macro */
# define CHARACTERISTIC_EXPAND_UNUSED__(Token) Token
# define CHARACTERISTIC_EXPAND__(x) CHARACTERISTIC_EXPAND_UNUSED__(x)
# define CHARACTERISTIC_STRINGIZE_UNUSED__(String) # String
# define CHARACTERISTIC_STRINGIZE(x) CHARACTERISTIC_STRINGIZE_UNUSED__(x)
# define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_stringize
# ( RefString ) CHARACTERISTIC_STRINGIZE( RefString )
#endif // CHARACTERISTIC_STRINGIZE
用法...
#if !defined(__cplusplus)
__characteristic__((error("C++ compiler required.")))
#elif !defined(_MSC_VER) && !defined( __GNUC__ ) &&
#!defined( __GNUG__ ) && !defined( __clang__ )
__characteristic__((warning("Unknown compiler details.")))
#endif
关注点
欢迎尝试 Compiler Explorer 上的示例。如果您这样做,则必须从 Msvc 版本的 inline 中删除 __pragma
语句,因为 CE 会拒绝它们。实际编译器接受它们,并且它们对于无条件启用内联是必需的,因为 __forceinline
关键字仅在选择了“仅内联”或“任何合适的”编译器开关时才有效。直接复制粘贴需要您清理该站点使用的行尾。最初,它被设计为仅与属性一起使用。但它经常在其他用途中派上用场,所以我认为应该分享。听取关于打破命名约定的反馈是本次练习的主要目标之一。
有关实际用例,请参阅 狂野西部编程:E.B.C.O. Compression。 特性标头 将与相关的文章一起更新。该库使用访问点 JOE
, 而不是 __characteristic__
,因为它反映了它所属库的名称。
历史
- 2021年8月22日:初始版本