19. 用if constexpr替换enable_if——带可变参数的工厂函数

# 19. 用if constexpr替换enable_if——带可变参数的工厂函数

C++17中最强大的语言特性之一是if constexpr形式的编译时if语句。它允许在编译时检查条件,并根据结果决定是否将代码纳入后续编译步骤。

在本章中,你将看到这个新特性如何简化代码。

# 问题

在《Effective Modern C++》的第18项中,斯科特·迈耶斯(Scott Meyers)描述了一个名为makeInvestment的方法:

template <typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);
1
2
3

这是一个工厂方法,用于创建Investment的派生类,其主要优点是支持可变数量的参数!

例如,以下是几个提议的派生类型:

// 文档中未提及variable_factory.cpp具体位置,仅按原文保留
// 基类:
class Investment {
public:
    virtual ~Investment() { }
    virtual void calcRisk() = 0;
};

class Stock : public Investment {
public:
    explicit Stock(const std::string&) { }
    void calcRisk() override { }
};

class Bond : public Investment {
public:
    explicit Bond(const std::string&, const std::string&, int) { }
    void calcRisk() override { }
};

class RealEstate : public Investment {
public:
    explicit RealEstate(const std::string&, double, int) { }
    void calcRisk() override { }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

书中的代码过于理想化,只有在所有类的输入参数数量和类型都相同时才有效:

斯科特·迈耶斯《Effective Modern C++修改历史和勘误表 (opens new window)》 :

makeInvestment接口不太现实,因为它意味着所有派生对象类型都可以由相同类型的参数创建。这在示例实现代码中尤为明显,其中参数被完美转发到所有派生类构造函数。

例如,如果有一个构造函数需要两个参数,而另一个构造函数需要三个参数,那么代码可能无法编译:

// 伪代码:
Bond(int, int, int) { }
Stock(double, double) { }

make(args...)
{
    if (bond)
        new Bond(args...);
    else if (stock)
        new Stock(args...)
}
1
2
3
4
5
6
7
8
9
10
11

现在,如果编写make(bond, 1, 2, 3),那么else语句将无法编译,因为不存在Stock(1, 2, 3)!为了使其正常工作,需要一个编译时if语句,排除不符合条件的代码部分。

在我的博客上,在一位读者的帮助下,我们提出了一个可行的解决方案(你可以在Bartek的编程博客《优秀的C++工厂实现2 (opens new window)》 中了解更多)。

以下是可行的代码:

template <typename... Ts>
unique_ptr<Investment>
makeInvestment(const string& name, Ts&&... params)
{
    unique_ptr<Investment> pInv;
    if (name == "Stock")
        pInv = constructArgs<Stock, Ts...>(forward<Ts>(params)...);
    else if (name == "Bond")
        pInv = constructArgs<Bond, Ts...>(forward<Ts>(params)...);
    else if (name == "RealEstate")
        pInv = constructArgs<RealEstate, Ts...>(forward<Ts>(params)...);
    // 调用其他方法初始化pInv...

    return pInv; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以看到,“神奇之处” 发生在constructArgs函数内部。

主要思路是,当Type可以由一组给定的参数构造时,返回unique_ptr<Type>;否则返回nullptr

# C++17之前的做法

在之前(C++17之前)的解决方案中,必须使用std::enable_if

// C++17之前
template <typename Concrete, typename... Ts>
enable_if_t<is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>>
constructArgsOld(Ts&&... params)
{
    return std::make_unique<Concrete>(forward<Ts>(params)...);
}

template <typename Concrete, typename... Ts>
enable_if_t<!is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>>
constructArgsOld(...)
{
    return nullptr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

std::is_constructible用于测试一组参数是否可用于创建给定类型。

快速回顾一下enable_if

enable_if(以及C++14起的enable_if_t)。它的语法如下:

template <bool B, class T = void>
struct enable_if;
1
2

当输入条件B为真时,enable_if的求值结果为T。否则,它属于SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)机制,特定的函数重载会从重载集中移除。

此外,在C++17中有一个辅助工具:

is_constructible_v = is_constructible<T, Args...>::value;
1

理论上,代码应该可以更简短一些。

不过,使用enable_if看起来既难看又复杂。C++17版本会怎样呢?

# 使用if constexpr

以下是更新后的版本:

template <typename Concrete, typename... Ts>
unique_ptr<Concrete> constructArgs(Ts&&... params)
{
    if constexpr (is_constructible_v<Concrete, Ts...>)
        return make_unique<Concrete>(forward<Ts>(params)...);
    else
        return nullptr;
}
1
2
3
4
5
6
7
8

甚至可以使用折叠表达式为其添加一些简单的日志记录功能:

template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs(Ts&&... params)
{
    cout << "func" << ":";
    // 折叠表达式:
    ((cout << params << ","), ...);
    cout << '\n ';

    if constexpr (std::is_constructible_v<Concrete, Ts...>)
        return make_unique<Concrete>(forward<Ts>(params)...);
    else
        return nullptr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

所有enable_if的复杂语法都不见了,甚至不需要为else情况编写函数重载。现在可以将富有表现力的代码封装在一个函数中。

if constexpr会计算条件,并且只会编译其中一个代码块。在这种情况下,如果一个类型可以由一组给定的参数构造,那么将编译make_unique调用。否则,返回nullptr(并且make_unique甚至不会被编译)。

你可以在Chapter If Constexpr Factory/variable_factory.cpp中运行这段代码。

# 总结

在本章中,你看到了if constexpr如何使代码更加清晰和富有表现力。在C++17之前,可以使用enable_if技术(SFINAE)或标签调度(tag dispatching)。但这些方法通常会生成复杂的代码,新手和非元编程专家可能很难读懂。if constexpr降低了有效编写模板代码所需的专业知识门槛。

上次更新: 2025/04/01, 13:21:34

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>