C++ 中的宏用法

我仍然看到很多地方#defines(在 C 和 C++ 中都有),而且我几乎可以肯定在大多数地方,这些并不是真正需要的。有时,我还了解到有一种方法可以用模板或其他构造来表达/替换宏,所以我最终决定列出我遇到的用例,其中用其他东西替换宏是有意义的,以及我不知道有任何替代方案。

注意:在这些注释中,我没有考虑外部代码生成器或可以更改已编译二进制文件的工具(例如,在.rodata部分中添加某些内容)。它将消除宏的更多用法(尤其是 X-Macro),但它需要更复杂的构建系统,特别是因为我们要么需要有一个预构建的代码生成器,要么使用我们的源代码为主机平台编译它(如果进行交叉编译,则不能总是自动完成)。

这并不意味着代码生成器不是一种可行的替代方案,恰恰相反。另一方面,在 C++ 源代码中更直接地表达逻辑或数据的可能性也有优势。

包括警卫

包含保护是我们首先想到的宏的例子,并且由于没有标准化的替代品(曾经有过提案,但没有太大兴趣),这是我们无法摆脱它们的主要原因。

模块,无论人们多么喜欢它们,还不能算作替代品。

当然有#pragma once,幸运的是,大多数(如果不是全部)编译器都支持它。

由于头文件保护在搜索其他宏时会增加噪音(例如,查找define,或者利用自动完成功能),这是我可以使用非标准语言扩展的少数地方之一。

#ifndef UNIQUE_MACRO
#define UNIQUE_MACRO

// content of header file

#endif

可以替换为

#pragma once

// content of header file

虽然#pragma仍然是一个预处理器指令,但我们至少可以避免使用宏。

注意📝
如果未来的某些编译器不支持此扩展,则可以使用某些脚本以编程方式#pragma once用唯一的包含保护来替换它。

“问题”#pragma once

忽略编译器可能不支持它的事实,对于那些声称这#pragma once不是一个可行的替代方案的人来说,因为它可能会在包含保护不会失败的情况下失败......我还没有找到真正重要的场景。

例如,假设一个 header guard 出现不止一次。这可能是因为

  • 头文件已被复制,其内容(包含保护除外)已被替换

  • 与版本控制的合并出现错误,导致一些文件重复(内容相同或不同)

  • 涉及挂载点和/或符号链接,因此可以从不同路径访问给定文件

  • 该文件在项目的另一部分被重复

在所有四种情况下,header guard 都会“起作用”,因为文件来自哪里并不重要,重要的是 guard 是否完全相同。它“起作用”是因为 guard 正在执行其工作,但代码可能仍然不是编译器,或者如果 header guard 保护的内容不相同,则可能会出现未定义的行为。

使用时#pragma once,取决于编译器如何实现它(例如通过内部映射文件名)可能会失败,从某种意义上说,您可能会收到编译器错误。

这是一件好事。

您复制了一个头文件,更改了内容,却忘记替换头文件保护?这是一个错误,代码可能无法编译(取决于头文件的包含顺序),但出现#pragma once此错误后这种可能性就不存在了。

一个文件被复制,最终发生了微小的更改(合并失败、文件复制到项目的另一部分并且未保持同步,……)。这是一个错误,可能会调用 UB(这是 ODR 违规),并且 header guard 可能更擅长隐藏此错误。如果它因编译失败#pragma once而是一件好事,而不是坏事。如果代码编译成功,程序可能不会按照程序员的意图运行

如果涉及多个挂载点或符号链接,并且同一个文件由不同的名称引用,那么也许是时候清理源目录以及代码的构建方式了,无论#pragma once是否失败。如果编译器无法区分两个文件名是否指向同一个文件……开发人员应该怎么做?这样的设置也可能会增加构建时间。

可能存在#pragma once确实不适合的情况,但我还没有遇到过。

避免函数调用

这对于 C 和 C++ 都适用。

考虑类似的事情

#define SUCCEEDED(hr) ((HRESULT)(hr) >= 0)

有人可能会认为使用宏更好,因为它可以确保代码内联/没有函数调用。不幸的是,智能编译器也可以将重复代码放入函数中,因为函数调用对于 C++ 规范来说是不可观察的。我不知道有哪个编译器实现了这样的功能,这很遗憾,因为它本来是重构的绝佳工具。

由于 C 和 C++ 都支持inline函数(这并不意味着函数被内联),并且有编译器特定的内联指令,因此似乎没有理由使用宏代替函数

inline bool succeeded(HRESULT hr) { return hr >= 0; }

有人可能会说,强制内联的指令是特定于编译器的,而宏适用于所有编译器,即使禁用了优化。这是真的,尽管如此,应该证明使用宏与使用函数相比有任何可衡量的好处(就像所有优化一样)。

另一个参数是succeeded将生成一个在结果二进制文件中占据某个位置的函数,但只有导出该函数时才成立。

succeeded此外,代码大小会影响性能,因此(在这种情况下不太可能)如果编译器选择不在每次调用时内联函数,则使用宏时生成的代码可能会更快。

TLDR;在使用宏而不是函数之前,最好阅读编译器文档并测试其优化标志以及它们如何转换生成的代码。

常量

对于不同类型的常量,有不同的方法。

积分

考虑

#define SIZE 5

在 C++ 中,对于整数值

const int size = 5;

是编译时常量,例如,可用于定义数组的大小或值enum。这是一条仅适用于整数值的特殊规则。

从 C++11 开始,constexpr是一个更好的选择

constexpr int size = 5;

因为它向人类和编译器都明确表明,这size是一个编译时值,意外更改应该是一个错误。它还使处理int与其他类型保持一致,因为

const float pi = 3.14;

不是编译时常量。

C 语言中的积分

即使在 C 中,对于整数值,也可以避免使用宏。没有constexpr,并且const int不能在编译时使用,但enum可以

enum{my_size=5};


int arr[my_size];

字符串文字

在许多情况下,应该使用constexpr std::string_viewconstexpr const char* const( C 语言中),但在某些情况下,宏方法仍然是必要的。constexpr

可以使用constexpr函数在编译时处理字符串,从而减少使用宏来连接文字的需要,但在某些地方,例如,static_assert仍然需要使用字符串文字。

#define VERSION "1.2.3"
/// ...

static_assert( /* ... */ , "Test on version " VERSION); // cannot use a constexpr string, it must be a literal

另一个优先使用宏(但不是必需的)而不是常量的情况是当对 的多次调用使用相同的格式字符串时printf。如果格式字符串和参数不一致,编译器会生成警告,但只有当传递给的参数printf是字符串文字时才有效。

在某些地方,即使是宏也不够好,我们需要复制粘贴字符串文字。一个典型的例子是使用assert

#define MESSAGE "Useful error message"
void foo(bool b){
  assert(b && "Useful error message");
}
void bar(bool b){
  assert(b && MESSAGE);
}

虽然它是实现定义的,但函数foo通常会在中止文本之前打印b && "Useful error message",而bar会打印b && MESSAGE

浮点数

#define PI 3.14 // and a lot of other digits

应替换为

constexpr double pi = 3.14 /* and a lot of other digits */;

其他类型

虽然有人可能会认为创建常量在二进制文件中占据了一定位置,但宏通常只是在多个位置复制粘贴相同的值。这意味着相同的值将在二进制文件中出现多次,除非优化器/链接器删除所有这些副本。

与宏相反,在 C++ 中添加全局变量可能会引发问题。由于全局变量可以执行代码(C 语言中不是这样),它们可以相互依赖,而且由于全局变量的初始化顺序未定义(它们甚至可能位于不同的库中),它们可能会访问未初始化的数据。

避免此问题的最简单方法是使所有全局变量constexpr(即编译时常量)或使用单例,这可以确保在需要依赖项的值时至少对其进行初始化。

使用成员变量和函数

有条件地调用成员函数

考虑一些类似的代码

struct s{
    bool is_feature_enabled();
    void execute_feature();
};

您应该execute_feature仅在必要时调用它is_feature_enabled(当然有更好的 API,但有时您会得到这样的结果)。

写起来很诱人

#define COND_EXEC_FEATURE(s, check, exec)\
    if(s.check()){s.exec();}

void foo(s& instance){
  COND_EXEC_FEATURE(instance, is_feature_enabled, execute_feature);
}

可以COND_EXEC_FEATURE用模板函数替换宏(如果它仅对给定类型有用,则不必将其模板化):

template <class C, class F1, class F2>
void cond_exec(C& c, F1 f1, F2 f2){
    if( (c.*f1)(1)){
        (c.*f2)(2);
    }
}

void bar(s& instance){
    cond_exec(instance, &s::is_feature_enabled, &s::execute_feature );
}

该语法看起来很奇怪,因为该运算符.*很少使用。

验证成员变量

#include <cassert>

// FIXME: replace with true variadic/recursive function
// For now, overload till 5 substructures should be good enough
// internal macro, do not call directly, see FEK_ASSERT_DEREF_CHAIN
#if defined(FEK_GET_MACRO) || defined(FEK_C_DEREF_CHAIN1) || defined(FEK_C_DEREF_CHAIN2) || defined(FEK_C_DEREF_CHAIN3)                        \
    || defined(FEK_C_DEREF_CHAIN4) || defined(FEK_C_DEREF_CHAIN5) || defined(FEK_CHECKED_DEREF_CHAIN) || defined(FEK_ASSERT_DEREF_CHAIN)
#error "*_DEREF_CHAIN* already defined"
#endif

#define FEK_C_DEREF_CHAIN1(ptr1, check_ptr) (check_ptr((ptr1)), void(), (ptr1))
#define FEK_C_DEREF_CHAIN2(ptr1, ptr2, check_ptr) (check_ptr((ptr1)), void(), FEK_C_DEREF_CHAIN1(ptr1->ptr2, check_ptr))
#define FEK_C_DEREF_CHAIN3(ptr1, ptr2, ptr3, check_ptr) (check_ptr((ptr1)), void(), FEK_C_DEREF_CHAIN2(ptr1->ptr2, ptr3, check_ptr))
#define FEK_C_DEREF_CHAIN4(ptr1, ptr2, ptr3, ptr4, check_ptr) \
        (check_ptr((ptr1)), void(), FEK_C_DEREF_CHAIN3(ptr1->ptr2, ptr3, ptr4, check_ptr))
#define FEK_C_DEREF_CHAIN5(ptr1, ptr2, ptr3, ptr4, ptr5, check_ptr) \
        (check_ptr((ptr1)), void(), FEK_C_DEREF_CHAIN4(ptr1->ptr2, ptr3, ptr4, ptr5, check_ptr))
#define FEK_GET_MACRO(_1, _2, _3, _4, _5, check_fun, NAME, ...) NAME

/// Usage:
/// Given a structure like  a->b->c , where every pointer is never nullptr, use
/// FEK_ASSERT_DEREF_CHAIN(a, b, c)  to make intent clear.
/// You can also write
///  auto ptr = FEK_ASSERT_DEREF_CHAIN(a, b, c)
/// and  ptr  will point to
///  a->b->c
/// If a substructure could be nullptr, you might prefer using FEK_CHECKED_DEREF_CHAIN, which accepts as last parameter a
/// function name (function can be overloaded) for checking if a structure is null, and that should throw in case of error).
#define FEK_CHECKED_DEREF_CHAIN(...) \
        FEK_GET_MACRO(__VA_ARGS__, FEK_C_DEREF_CHAIN5, FEK_C_DEREF_CHAIN4, FEK_C_DEREF_CHAIN3, FEK_C_DEREF_CHAIN2, FEK_C_DEREF_CHAIN1) \
        (__VA_ARGS__)
#define FEK_ASSERT_DEREF_CHAIN(...) FEK_CHECKED_DEREF_CHAIN(__VA_ARGS__, assert)

struct S1{
    int data;
};
struct S2{
    S1* ptr1;
};
struct S3{
    S2* ptr2;
};


void throw_on_null(const void* ptr){if(ptr == nullptr){throw 42;}}

int bar(S3* ptr3){
    auto v = FEK_CHECKED_DEREF_CHAIN(ptr3, ptr2, ptr1, throw_on_null)->data;
    return v;
}

另外,在这种情况下,可以删除宏:

template <class F, class C1>
auto deref_chain(F f, C1* p1){
    f(p1);
    return p1;
}

template <class F, class C1, class C2, class... CN>
auto deref_chain(F f, C1* p1, C2 p2, CN... cn){
    f(p1);
    return deref_chain(f, p1->*p2, cn...);
}

template <class C1>
auto assert_deref_chain(C1* p1){
    assert(p1 != nullptr);
    return p1;
}

template <class C1, class C2, class... CN>
auto assert_deref_chain(C1* p1, C2 p2, CN... cn){
    assert(p1 != nullptr);
    return assert_deref_chain(p1->*p2, cn...);
}

struct S1{
    int data;
};
struct S2{
    S1* ptr1;
};
struct S3{
    S2* ptr2;
};

int bar(S3* ptr3){
    auto v = deref_chain(throw_on_null, ptr3, &S3::ptr2, &S2::ptr1)->data;
    return v;
}

唯一的缺点:使用函数重载比较困难,如果想使用assert,则需要编写单独的函数,因为assert它本身不是函数,因此不能用作参数。

在回调中使用宏

在前面的例子中,我必须定义一个单独的模板函数assert_deref_chain,因为assert它不是函数,因此不能作为参数传递

std::vector<int*> v;
// verify all values are != nullptr
std::for_each(v.begin(), v.end(), assert);

如果宏为函数设置了别名,并且不是函数宏,则这不是问题。例如:

#define FEK_WRAP_ASSERT [](auto v){assert(v);}


std::vector<int*> v;
// verify all values are != nullptr
std::for_each(v.begin(), v.end(), FEK_WRAP_ASSERT);

有人可能会说这FEK_WRAP_ASSERT没有必要,在这种情况下,它没有任何用处。如果它被彻底修改并部分重新实现,assert如下所示:

#define FEK_WRAP_ASSERT [func = __func__](auto v){ /* if not v, print an error message and abort */ }

然后我们可以将宏写入(而不是调用!)的控制台进行打印,而这无法通过函数完成。我不确定是否有有效的用例。

有条件返回

假设您有一个 API,其中包含多个接受某个输入参数的函数,如果该参数不符合特定条件,则该函数将返回特定的错误代码。此检查可以包装在宏中并在每个函数中重复使用。

#define FEK_RETURN_IF_VERIFY_FAILS(p) \
  do{ if(verify(p)){ return errorcode::wrong_param; } }while(false)

errorcode foo(int i){
  FEK_RETURN_IF_VERIFY_FAILS(i);
  // business logic of foo
  return errorcode::success;
}

errorcode bar(const std::string& s){
  FEK_RETURN_IF_VERIFY_FAILS(s);
  // business logic of bar
  return errorcode::success;
}

一般来说,在宏中写return(或breakcontinuegoto)不是一个好习惯,因为它会在读者不知情的情况下改变控制流(即使对于异常也是如此……)。希望宏的命名…​RETURN_IF…​能够让意图足够清晰。

在这种情况下,无法用函数替换该宏,因为函数无法强制调用者返回。

如果准备添加另一层间接层,则可以删除该宏并重组其代码:

template <class T, class F>
errorcode exec_if_verify(T&& p, F f){
    if(verify(p)){ return errorcode::wrong_param; }
    return f(std::forward<T>(p));
}

errorcode foo(int i){
    return exec_if_verify(i,
        [](auto i) {
            // business logic of foo
            return errorcode::success;
        }
    );
}

errorcode bar(const std::string& s){
    return exec_if_verify(s,
        [](auto s) {
            // business logic of bar
            return errorcode::success;
        }
    );
}

虽然这种方法允许删除宏,但代码不一定更容易理解。

确实,没有隐藏的返回,但这种方法需要复制函数签名、添加其他无用的作用域、使用 lambda 进行间接寻址,并用大量样板代码替换一行。它还会向用户隐藏是否执行了foo或的业务逻辑bar,因为参数验证和分支在其他地方。如果没有 lambda,则需要编写更多代码,因为它需要复制函数签名。

至少对于简单情况来说,生成的代码是相同的。

如果有很多函数需要以类似的方式进行参数验证,我仍然会使用宏。模板化方法的可读性并不高,而且通常更难维护。

寿命延长

构造函数内部

我认为添加宏的一个用例是在处理constexpr 数据结构时。

考虑

struct data{ int i; };

struct node{
    data d;
    const node& next;
};
constexpr const node Null{data{0}, Null};
#define FEK_NODE(d, next) node{d.i == 0 ? data{42} : d, next} // or throw, stop compilation, or something else

constexpr node leaf(data d){
    return FEK_NODE(d, Null);
}

constexpr const node list{
    data{-1}, FEK_NODE(data{0}, leaf(data{1}))
};

添加构造函数node(或工厂函数)将创建悬空引用( 除外leaf),因为它会禁用生命周期扩展。宏FEK_NODE不存在同样的问题,因为它不会引入新的作用域。

在 lambda 捕获中,作为强制转换的替代/std::as_const

当使用时mutexed_obj,我遇到了类似的生命周期扩展问题,并且宏再次有助于减少需要编写的代码量。

给定一个与本文mutexed_obj中介绍的类类似的类,以及以下结构:

struct data{
    std::string str = "Hello World!";
    int i = -1;
};

struct s {
    // type with no default constructor, or expensive default constructor,
    // and possibly no move or copy constructors (or expensive)
    s(bool, const std::string&);
};

考虑以下函数:

bool foo(mutexed_obj<data>& d_m){
    // ...
    const s val = lock(d_m, [](data& d){
        bool res = d.str == get_value();
        if(res){d.i++;}
        return s(res, d.str);
    });
    // ...
}

get_value是在其他地方声明的函数,我们甚至可能无法控制它。它的签名是以下之一,哪一个都无所谓:

const std::string& get_value();
// or
std::string get_value();
// could also be std::string& get_value(), std::string&& get_value(), ... but leaving out for simplicity

函数内部发生的事情foo至少是次优的,可能有问题,因为代码get_value在锁定互斥锁时调用外部/未知代码()。

解决该问题的第一种方法是直接get_value调用foo

bool foo(mutexed_obj<data>& d_m){
    // ...
    const auto& value = get_value(); // or const std::string& value = get_value();
    const s val = lock(d_m, [&value](data& d){
        bool res = d.str == value;
        if(res){d.i++;}
        return s(res, d.str);
    });
    // ...

副作用是,结果的范围get_value要大得多。如果get_value按值返回,则只有存在时才会释放内存foo。如果范围尽可能小就更好了。

写作

bool foo(mutexed_obj<data>& d_m){
    // ...
    s val;
    {
        const auto& value = get_value(); // or const std::string& value = get_value();
        val = lock(d_m, [&value](data& d){
            bool res = d.str == value;
            if(res){d.i++;}
            return s(res, d.str);
        });
    }
    // ...
}

可能会或可能不会起作用,这取决于s构造函数,但通常不可能做到这一点const,除非使用第二个 lambda:

bool foo(mutexed_obj<data>& d_m){
    // ...
    s val = [](){
        const auto& value = get_value(); // or const std::string& value = get_value();
        return lock(d_m, [&value](data& d){
            bool res = d.str == value;
            if(res){d.i++;}
            return s(res, d.str);
        });
    }();
    // ...
}

这……并不是人们会写的第一件事。考虑到 lambda 可以在捕获变量时调用函数(自 C++14 起),可以这样写

bool foo(mutexed_obj<data>& d_m){
    // ...
    const s val = lock(d_m, [value = get_value()](data& d){
        bool res = d.str == value;
        if(res){d.i++;}
        return s(res, d.str);
    });
    // ...
}

这个例子也有一个缺点。如果get_value返回一个 const 引用,我们就做了一个不必要的复制,而之前所有的解决方案都没有这样做。

为了避免这种不必要的复制,可以这样写

bool foo(mutexed_obj<data>& d_m){
    // ...
    const s val = lock(d_m, [&value = get_value()](data& d){
        bool res = d.str == value;
        if(res){d.i++;}
        return s(res, d.str);
    });
    // ...
}

但是如果按值返回,此代码将无法编译get_value,因为 lambda 按引用捕获,而不是 const 引用。错误消息实际上是(使用 GCC)error: non-const lvalue reference to type 'basic_string<…​>' cannot bind to a temporary of type 'basic_string<…​>':。

可以用来std::as_const创建临时引用,但是

  • 它无法编译,因为std::as_const它不接受右值作为参数

  • 即使它可以在右值的情况下进行编译,它也会创建一个悬垂引用。

可以std::as_const手动执行操作,即添加const强制类型转换,然后让 lambda 捕获推断类型

bool foo(mutexed_obj<data>& d_m){
    // ...
    const s val = lock(d_m, [&value = static_cast<const std::string&>(get_value())](data& d){
        bool res = d.str == value;
        if(res){d.i++;}
        return s(res, d.str);
    });
    // ...
}

这样做可以正常工作,但代码非常冗长,可能会意外引入不必要的转换(例如,如果get_value返回 a char*)此外,在这个例子中,lambda 中只捕获了一个函数的输出。如果有两个或三个,代码的可读性就会大大降低。

因为函数不起作用(它会创建悬垂引用),所以宏似乎是减少样板的唯一可行方法:

#define FEK_C_REF(...) static_cast<const std::remove_reference_t<decltype(__VA_ARGS__)>&>(__VA_ARGS__)

bool foo(mutexed_obj<data>& d_m){
    // ...
    const s val = lock(d_m, [&value = FEK_C_REF(get_value())](data& d){
        bool res = d.str == value;
        if(res){d.i++;}
        return s(res, d.str);
    });
    // ...
}

数组中元素的数量

众所周知,在 C 语言中,人们必须使用类似于的宏(注意:不要使用它,请参阅此处以了解更好的替代方案)

#define ARR_SIZE(array) (sizeof(array)/sizeof(array[0]))

获取数组中元素的数量。

在 C++ 中,这种模式通常被类似这样的函数替换

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept
{
  return N;
}

不幸的是,仍然有几个地方ARR_SIZE可能需要类似的宏。

更多详细信息可在此处找到,其中包含用于 C 的类型安全宏和用于 C++ 的类型安全宏。

字符串化

使用宏,可以将一段代码“字符串化”。例如,这可以用来创建更强大的日志系统

#include <iostream>

#define PRINTVAR(V) {std::cout << #V " = " << V;}

void foo(){
  int a = 42;
  PRINTVAR(a);
}

执行时foo,该函数将打印a = 42

目前,语言中没有其他与字符串化等效的构造。

条件编译

对于编译特定于平台的代码或在编译时启用禁用功能(如运行时检查)很有用。

// foo.h
#ifdef FOO
void foo();
#endif

或者,这些情况可以由构建系统处理。

例如,可能有两个foo.hpp文件,一个是空文件,另一个包含函数的声明。对于未定义的foo目标,将包含空文件。FOOfoo.hpp

类似地,构建系统可以通过编译一个或另一个源文件来帮助提供不同目标的实现。

如果文件内部只有很小的子集有所不同,则这种方法的扩展性不佳。

因此在某些情况下可以避免使用宏,但通常,它们比使用构建系统更具可读性和易于维护。

可选变量

这些参数适用于局部变量、全局变量和成员变量。

如果成员变量是“可选的”,如“其存在取决于”构建系统,表达这种意图的最简单方法是使用宏

struct s{
  int i;
  #ifdef S_WITH_J
  int j;
  #endif
};

可以使用或不使用宏来构建代码S_WITH_J,也需要使用宏S_WITH_J来有条件地访问成员变量j

在某些情况下可以避免使用宏。

例如,借助辅助类和一些模板机制,可以编写

struct with_j {
  int j;
};

struct without_j { };


struct s : std::conditional_t< /* condition */ , with_j, without_j> {
  int i;
};

使用此技术,不依赖于宏,而是依赖于模板参数。这提供了更大的灵活性;例如,只有当另一个结构也具有给定的成员变量时s,才可以表示具有s成员变量。j

不幸的是,使用成员变量并不容易。即使使用if constexpr,尝试访问成员变量j也会导致编译器错误。

最简单的方法是将所有访问封装在j成员函数中,并提供等效的模拟without_j

也可以使用免费功能来完成,但需要多加注意。

这种方法的另一个缺点是成员变量的顺序受到影响。

使用这种技术,可选成员变量被放在开头,而使用宏,可以很容易地将它们与其他变量混合。

给定函数签名,并且如果空间不是问题,则使用带有无操作函数的虚拟变量可能是避免使用宏的最简单方法。

宏的主要缺点是,即使代码可能更易于阅读和维护,但它不会被编译,甚至不会被验证语法的正确性。

处理__func__/std::source_location

因为引入新函数(lambda 也是如此)意味着改变 的值__func__,如果我们想保持它不变,通常没有什么可以替代宏的。即使添加了std::source_location,也有一些用例没有涵盖。例如,可变函数不能有默认参数,因此std::source_location作为参数并不那么有用,而其他函数(如二元运算符或重写函数)不能添加其他参数。

统一重载中的 C 函数

在处理提供 C API 的库时,经常会发生多个不同的函数,在 C++ 中这些函数通常通过函数重载来处理。

例如,C 标准库定义了abs、labsfabs、fabsffabsl和其他函数。调用“错误”函数不会产生编译时错误,但会导致隐式转换并产生不良影响。在 C++ 中,有一个重载函数std::abs可以避免这些类型的错误。

在某些情况下,宏可以帮助“生成”一些粘合代码,特别是当函数名称包含类型时。

例如:

// from the C API
int foo_int(int);
long foo_long(long);


// macro to generate glue code
#define TO_OVERLOAD(type, name) inline type name(type v){return name##_##type(v);}

// generate overload set
TO_OVERLOAD(int, foo)
TO_OVERLOAD(long, foo)


// forget about foo_int and foo_long, just use foo, it will call the correct overload
i = foo(i);

常用函数

有时宏用于实现常见功能。首先想到的例子是operator!=按照以下方式实现operator==

#define FEK_OP_UNEQUAL_INLINE(type) friend bool operator!=(const type& lhs, const type& rhs)noexcept(noexcept(rhs == lhs)){ return not (rhs == lhs);}

struct s{
    friend bool operator==(const s& a, const s& b);
    FEK_OP_UNEQUAL_INLINE(s);
};

// or

#define FEK_OP_UNEQUAL_DECL(type) friend bool operator!=(const type& lhs, const type& rhs)noexcept(noexcept(rhs == lhs))
#define FEK_OP_UNEQUAL_IMPL(type) bool operator!=(const type& lhs, const type& rhs){ return not (rhs == lhs);}
struct s{
    friend bool operator==(const s& a, const s& b);
    FEK_OP_UNEQUAL_DECL(s);
};

// in the .cpp file
FEK_OP_UNEQUAL_IMPL(s)

使用 CRTP 模式可以避免这种情况:

template<typename T>
class gen_uneq_from_eq {
    friend bool operator!=(const T& lhs, const T& rhs) noexcept(noexcept(rhs == lhs)){ return not (rhs == lhs); }
};


struct s : private gen_uneq_from_eq<s> {
    friend bool operator==(const s& a, const s& b);
};

CRTP 模式的主要缺点是它强制将实现放在头文件中,因为整个类都是模板化的,而使用宏方法,可以拆分定义和实现。operator!=这应该不是问题,但如果基类的功能需要包含最终用户不需要的一些其他头文件,则至少会对编译时间产生负面影响。

X-微距

enum class e {e1,e2,e3,e4,e5,e6,e7,e8};

const char* to_string(e v){
  switch(v){
      case e::e1: return "e::e1";
      case e::e2: return "e::e2";
      case e::e3: return "e::e3";
      case e::e4: return "e::e4";
      case e::e5: return "e::e5";
      case e::e6: return "e::e6";
      case e::e7: return "e::e7";
      case e::e8: return "e::e8";
  }
}

对比

enum class e {e1,e2,e3,e4,e5,e6,e7,e8};

const char* to_string(e v){
  switch(v){
    #define CASE_RET_LIT(v) case v: return #v
    CASE_RET_LIT(e::e1);
    CASE_RET_LIT(e::e2);
    CASE_RET_LIT(e::e3);
    CASE_RET_LIT(e::e4);
    CASE_RET_LIT(e::e5);
    CASE_RET_LIT(e::e6);
    CASE_RET_LIT(e::e7);
    CASE_RET_LIT(e::e8);
    #undef CASE_RET_LIT;
  }
}

一些库无需借助宏(至少是用户可见的宏)即可解决这个问题,但它们通常依赖于编译器扩展,因此依赖于编译器供应商和版本。

X-macros 可用于进一步减少编写的代码:

#define ENLIST_ENUMS(X) \
    X(e1) \
    X(e2) \
    X(e3) \
    X(e4) \
    X(e5) \
    X(e6) \
    X(e7) \
    X(e8) \

enum class e {
    #define AS_VALUE(a) a ,
    ENLIST_ENUMS(AS_VALUE)
    #undef AS_VALUE
};

const char* to_string(e v){
  switch(v){
    #define CASE_RET_LIT(a) case e::a: return "e::"#a;
    ENLIST_ENUMS(CASE_RET_LIT)
    #undef CASE_RET_LIT
  }
}

此外,X-Macros 还可以创建更复杂的数据结构,而不仅仅是将枚举映射到文字(反之亦然)

例如,考虑一下通常的表格数据结构

item     | property1   | property2   | property3   | ...

item_a   | property1_a | property2_a | property3_a | ...
item_b   | property1_b | property2_b | property3_b | ...
item_c   | property1_c | property2_c | property3_c | ...
...

由于constexpr,可以在编译时创建这样的表/矩阵,但有时就像将枚举映射到某个字符串一样,使用宏可以进一步减少代码重复(并生成更紧凑的代码)

// file.mpp
X(item_a, property1_a, property2_a, property3_a, ...)
X(item_b, property1_b, property2_b, property3_b, ...)
X(item_c, property1_c, property2_c, property3_c, ...)
....

一个更具体的例子:对神奇宝贝的不同信息进行编码:

// pokemon.mpp
// Name,       if starter,    possible evolutions, eventually other data...
X(Bulbasaur,   starter::yes,  EVOLVES_TO(Ivysaur))
X(Ivysaur,     starter::no,   EVOLVES_TO(Venusaur))
X(Venusaur,    starter::no,   EVOLVES_TO())
X(Charmander,  starter::yes,  EVOLVES_TO(Charmeleon))
X(Charmeleon,  starter::no,   EVOLVES_TO(Charizard))
X(Charizard,   starter::no,   EVOLVES_TO())
// ...
X(Eeve,        starter::yes,  EVOLVES_TO(Vaporeon, Jolteon, Flareon))
X(Vaporeon,    starter::no,   EVOLVES_TO())
X(Jolteon,     starter::no,   EVOLVES_TO())
X(Flareon,     starter::no,   EVOLVES_TO())
// ...

请注意,我使用它.mpp作为文件扩展名,因为它不是有效的 C++ 代码(除非X定义为某些内容),因此它既不是头文件也不是实现文件。

// pokemon.hpp
#pragma once
enum class starter{yes,no};
using id_helper = char[__LINE__+3];
enum pokemon {
#define X(name, starter, evolves) name = __LINE__ - sizeof(id_helper),
#include "pokemon.mpp"
#undef X
};
};

std::string_view name(pokemon p);
int id(pokemon p);
std::span<pokemon> evolutions(pokemon);
}
// pokemon.cpp

// helper for creating a local array with EVOLVES_TO
template <std::size_t N>
constexpr std::array<pokemon, N> to_evolutions(const pokemon (&a)[N]){
  return std::to_array(a);
}
constexpr std::array<pokemon, 0> to_evolutions(int){
  return {};
}

const std::string_view name(pokemon p) {
  switch (p) {
#define X(name, starter, evolves) case name: return #name;
#include "pokemon.mpp"
#undef X
  }
}

bool id(pokemon p) {
  static_assert(static_cast<int>(Bulbasaur)==1, "sanity check");
  return static_cast<int>(p);
}

std::span<const pokemon> evolutions(pokemon p){
  switch (p) {
#define EVOLVES_TO(...) {__VA_ARGS__}
#define X(name, starter, evolves) case name : {static constexpr auto evolutions = to_evolutions( evolves ); return evolutions;}
#include "pokemon.mpp"
#undef X
#undef EVOLVES_TO
  }
}

请注意,所有函数都不需要任何内存分配,并且可以在编译时使用,只需将实现移至标题并添加即可constexpr。编写替代实现而不使用宏会增加大量样板和重复代码,这使得维护更加困难且更容易出错。

类生成器

与统一 C 函数重载类似,宏可用于围绕某些标记生成类。

C 库具有处理资源的函数,在 C++ 中,这项工作通常最好使用析构函数来解决

#define UNARY_FUNC_TO_STRUCT(type, func_name, struct_name) \
  struct struct_name { \
    auto operator()(type* ptr) const { \
      return func_name(ptr); \
    } \
  }

#define TYPEDEF_UNIQUE_PTR(type) \
  UNARY_FUNC_TO_STRUCT( type, type##_free, type##_deleter); \
  using unique_##type = std::unique_ptr<type, type##_deleter>


// Declares unique_BUF_MEM
// a unique_ptr wrapping BUF_MEM with BUF_MEM_free as deleter
TYPEDEF_UNIQUE_PTR(BUF_MEM);

当然,使用C++20你可以直接使用 lambda

// Declares unique_BUF_MEM
// a unique_ptr wrapping BUF_MEM with BUF_MEM_free as deleter
using unique_BUF_MEM = std::unique_ptr<BUF_MEM, decltype([](auto* ptr){BUF_MEM_free(ptr);})>;

但是宏可用于同时声明多个事物;例如工厂函数

#define TYPEDEF_UNIQUE_PTR_AND_FACTORY(type) \
  using unique_##type = std::unique_ptr<type, decltype([](auto* ptr){type##_free(ptr);})>; \
  inline unique_##type create_##type() { \
    return unique_##type(type##_new()); \
  } \
  static_assert(true)


// Declares unique_BUF_MEM
// a unique_ptr wrapping BUF_MEM with BUF_MEM_free as deleter
// and the factory function create_BUF_MEM
TYPEDEF_UNIQUE_PTR_AND_FACTORY(BUF_MEM);

再次强调,宏并不是必需的,因为代码相当于

// Declares unique_BUF_MEM
// a unique_ptr wrapping BUF_MEM with BUF_MEM_free as deleter
using unique_BUF_MEM = std::unique_ptr<BUF_MEM, decltype([](auto* ptr){BUF_MEM_free(ptr);})>;
unique_BUF_MEM create_BUF_MEM() {
  return unique_BUF_MEM(BUF_MEM_new());
}

如果你想毒害C 接口的话,这会变得更加冗长。

短路

使用宏通常可以实现短路,而函数参数总是会被评估,即使函数是完全内联的(因为内联不能改变一个编写良好的程序的行为)。

#define LAZY_COMPARE_EQ_IF_NOTNULL_BAD(a, b) ((a) != nullptr) and ((b) != nullptr) and ((a) == (b))
#define LAZY_COMPARE_EQ_IF_NOTNULL_BETTER(a_, b_) [](){const void* a = a_; if(a==nullptr) return false; const void* b = b_; return (b != nullptr) and (a == b);}()

bool compare_eq_if_notnull(const void* a, const void* b){
  return (a != nullptr) and (b != nullptr) and (a == b);
}

int* g() noexcept;
const int* h() noexcept;

void foo(){
  // does not call h() if g() returns null, but might call the functions more than once
  if(LAZY_COMPARE_EQ_IF_NOTNULL_BAD(g(), h())){
  }

  // does not call h() if g() returns null, every function called at most once
  if(LAZY_COMPARE_EQ_IF_NOTNULL_BETTER(g(), h())){
  }

  // calls g and h once, always
  if(compare_eq_if_notnull(g(), h())){
  }
}

通过添加另一层间接层,可以解决这个问题,而无需借助宏

template <class F, class G>
bool compare_eq_if_notnull(F f, G g){
  const void* a = f();
  if(a == nullptr) return false;
  const void* b = g();
  return (b != nullptr) and (a == b);
}
void foo(){
  // does not call h() if g() returns null, every function called at most once
  // just like COMPARE_EQ_IF_NOTNULL_BETTER
  if(compare_eq_if_notnull([]{return g();}, []{return h();})){
  }
}

优点是,除了我们不再使用宏之外,它看起来和工作起来都像“普通”C++ 代码。没有短路,函数参数(lambda)被评估,但g只有h当 lambda 在函数内部执行时才会评估。另一方面,我们需要记住每次要使用时都添加这个(详细的)间接compare_eq_if_notnull,而使用宏则没有必要。

尽可能利用编译器扩展

如果编译器扩展不会改变程序的行为、增强诊断功能并提供查找或避免错误的额外可能性,则它们特别有用。

例如,Microsoft 编译器支持SAL 注释 🗄️。由于其他编译器不支持它,因此将其包装在宏中可确保代码仍可使用其他编译器进行编译,但当使用 MSVC 进行编译时,我们可以利用特定于编译器的诊断。

自 C++11 以来,注释有了标准化的语法,但并非所有编译器都支持具有标准语法的所有属性,并且并非所有注释都可以映射到标准化语法。

因此,在可能的情况下使用标准化语法可以避免通过宏添加间接寻址,从而使代码具有可移植性。

注释由编译器或外部工具提供,因此除了给定的工具之外,宏不应该执行任何操作。它是一种条件编译的形式。

此外,一些注释可以用适当的语言结构或专门设计的类来替换,例如std::string_view、、等等。这些库工具是一种更好的解决方案std::spanstd::unique_ptr因为所有支持 C++ 的工具(尤其是编译器)都应该理解这些结构,它们可以在运行时和编译时设置不变量。

导出函数和类

宏通常用于从库中导出函数和宏:

struct MYPROJ_EXPORT s {
  // ...
};

MYPROJ_EXPORT将解析为无__declspec(dllexport),,,,或其他内容__attribute__visibility("default")__declspec(dllimport)具体取决于代码的编译方式。

在 Windows 上,主要的替代方法是使用.def文件。在非 Windows 系统上,根据您的工具链,可能会有类似的可用方法。

尤其是对于不依赖于特定平台或工具链的应用程序,使用宏是最具可移植性的方法。由于 C++ 标准没有定义库和导出函数,因此在很长一段时间内,甚至永远都不会有可移植的替代方案。

宏指南

何时使用宏

当没有其他方法可以减少代码重复时使用它。

由于宏是比函数低级的工具,因此总会有其有效用例。但由于它们有明显的缺点,因此在不需要时应避免使用它们。

保持实用:如果它使代码更容易理解和维护,就使用它们(例如,单个文件包含一个或多个#ifdef重复内容,而多个文件则包含重复内容),但如果优势可以忽略不计,则不要使用,因为工具(和人类)更难理解周围的代码,因为宏可以隐藏很多关于程序结构的相关信息。

为了避免错误,请尽可能减少宏中封装的范围/功能;尽快将工作转发给 C++ 构造(如类或函数)。宏应该完成其他东西无法完成的事情。

将宏设为大写并加上前缀

最好是两者兼有,至少要有一个,永远不要没有。此规则是为了确保不会与“正常”代码发生冲突,因为宏优先于其他 C++ 构造。

多语句宏

由多个语句组成的宏可能会有问题,因为它们可能会意外更改周围代码的含义。有多种方法,具体取决于宏的使用方式,特别是如果我们想要分配一些结果或更改控制流(return、break、continue、goto)。

// = nothing
// * statement can be split by accident (for example when writing if(cond) FOOBAR();)
// * forces end-user to write ;
// * can change control flow (return, goto, break and continue)
// * result of the first statement can be assigned
// * can define variables that might clash with others in the same scope
#define FOOBAR() foo(); bar()

// = scope
// * statement is always intact
// * cannot force user to write ; at the end and if the users writes ; at the end, it can create invalid code if(cond) FOOBAR(); else FOOBAR();
// * can change control flow (return, goto, break, and continue)
// * can define local variables (no clashes)
// * cannot assign the result of any statement to a variable
#define FOOBAR_SCOPE() {foo(); bar();}

// = do{...}while(false)
// * statement is always intact
// * can change control flow (return, goto), but no break or continue
// * cannot assign the result of any statement to a variable
// * forces end-user to write ;
#define FOOBAR_DOWHILEFALSE() do{foo(); return bar();}while(false)

// comma op
// * statement is always intact
// * cannot change control flow (return, goto break, and continue)
// * cannot assign the result to a variable (except the result of the last statement)
// * comma operator can be overloaded, use void() to avoid unexcepted function calls
// * cannot define local variables
// * forces end-user to write ;
// * usable in c++11 constexpr context
#define FOOBAR_COMMA() (foo(), void(), bar())

// lambda (since c++11)
// * statement is always intact
// * cannot change control flow (return, goto, break, and continue)
// * can assign the result of any statement to a variable
// * can define local variables
// * changes __func__, unless used in the capture (possible since c++14),
//   it is otherwise equivalent to writing a separate function and using it in the macro directly
// * can be passed as callbacks to other functions
// * if written without parenthesis "()", and not passed as a parameter, it's not a compilation error,
//   hopefully the compiler reports it as unused, as it is probably an error
#define FOOBAR_LAMBDA [](){foo(); return bar();}


// immediately invoked lambda (since c++11)
// * statement is always intact
// * cannot change control flow (return, goto, break, and continue)
// * can assign the result of any statement to a variable
// * can define local variables
// * changes __func__, unless used in the capture (possible since c++14), or passed as param to the lambda
//   it is otherwise equivalent to writing a separate function and using it in the macro directly
#define FOOBAR_INVOKEDLAMBDA() [](){foo(); return bar();}()

如表:

构造声明完整力量;控制流(抛出除外)分配结果局部变量可用作回调

没有什么

是的

返回,转到,中断,继续

第一份声明

范围

是的

返回,转到,中断,继续

是的

当为假时执行

是的

是的

返回,转到

是的

逗号

是的

是的

最后声明

拉姆达

是的

是的

任何声明

是的

是的

调用 lambda

是的

是的

任何声明

是的

作为指导原则,我会使用立即调用的 lambda,因为它减少了宏对函数的操作,并且非常灵活,因为它可以像真正的函数一样返回值。do-while-false例如,当人们想要有条件地从函数返回时,这种方法很有用。

如果 break需要或continue,请考虑将循环放在单独的函数中。这样就可以do-while-false使用return语句重复使用该技巧。否则,请考虑使用goto

逗号运算符在 C++11 上下文中最有用constexpr

对于单语句宏,这些解决方法都不是必需的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值