狂野西部编程:宏






4.64/5 (3投票s)
标准宏的一种替代解决方案,旨在让宏不那么“邪恶”。
引言
宏基于 #define
指令,该指令指定一个宏,由一个标识符名称和一个要由预处理器在源代码中该宏的每个出现处进行替换的 字符串
或数字组成。宏通常用于抽象 pragma、declspecs、属性、先决条件和功能检查。
许多程序员 相信 知道宏是邪恶的。主要原因之一是它们会污染代码库,因为它们会覆盖同名的任何内容。话虽如此,它们通常是跨平台访问编译器特定功能的唯一方法。宏需要在两个事物之间走一条微妙的路线。首先是唯一命名。这涉及到代码库污染,因为宏是全局的。它们通常在第一次使用之前定义,在最后一次使用之后立即取消定义。这有助于抵消污染。第二个 concerns 是让名称有意义。这两者的结合常常导致结果不理想。作为替代,我们将提供一个使用属性风格命名(attribute-style naming)的可移植性包装器,旨在解决此冲突。
背景
在处理 CppCon 上一个视频提出的代码库挑战时,我考虑了一些关于编程中一些更有争议的主题的替代方案。这些想法最初纯粹是学术性的,但似乎有广泛的实际用途。尽管其中一些替代方案需要库结构来支持它们,这也需要解释。一个想法开始形成。一系列旨在解决编码中有争议的概念并提供替代方法和手段的文章。其中第一个将是最基础的。宏。
设计
我称之为“特性”(characteristic)的基本设计是一个宏,它本质上是四个独立的宏协同工作。第一个是分派 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 的修饰符,以及其他一些东西,以便在语言本身中实现此功能。但我离题了,所有宏都是大写命名,除了末尾的名称。请注意这一点,因为稍后会有更多关于它的内容。
下一个宏是转发宏。它的作用正如其名称所示。它转发给定的令牌。在此宏中,给定的令牌将与前缀 CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_
连接,该前缀将扩展为我们相应的 ID。
# define CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__( __DISPATCH_ID__, ... ) \
CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_ ## \__DISPATCH_ID__
__VA_ARGS__ __DISPATCH_ARGS__EMPTY__
下一个宏是分派宏本身。它调用转发宏。此调用通过添加第二组括号来完成对转发器的调用。这使我们拥有了属性风格的语法,同时确保在各种编译器上实现所需的扩展。
# define CHARACTERISTIC_DISPATCHER( ... ) \
CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__ __VA_ARGS__ __DISPATCH_ARGS__EMPTY__
最后一个宏是访问宏,它仅用于取消定义。由于 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 的宏非常简单。作为额外的奖励,关键字具有额外的语法高亮显示,属性说明符具有对应的类型名称。再次强调,关键字、类和变量名称是首选,因为它们不太可能受到其他宏的影响。这就是为什么在标识符的末尾使用小写字母,因为小写字母不太可能成为另一个宏。但这些不限于单个标识符。它们可以支持带参数的函数式宏。例如……
//__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
关键字只有在选择了“仅内联”或“任何合适”的编译器开关时才有效。直接复制粘贴需要您清理该站点使用的行尾。最初,它设计为仅与属性一起使用。但由于它经常很有用,所以我认为值得分享。听到关于打破命名约定的反馈是这项练习的主要目标之一。
有关实际用例,请参阅 Wild West Coding: E.B.C.O. Compression。 特性头文件将与它们相关的文章一起更新。该库使用访问点 JOE
,而不是 __characteristic__
,因为它反映了它所属的库的名称。
历史
- 2021年8月22日:初始版本