CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • C++17 详解 说明
  • 第一部分——语言特性
  • 1. 快速入门
  • 2. 移除或修正的语言特性
  • 3. 语言澄清(Language Clarification)
  • 4. 通用语言特性
  • 5. 模板(Templates)
  • 6. 代码标注
  • 第二部分 - 标准库的变化
  • 7. std::optional
  • 8. std::variant
  • 9. std::any
  • 10. std::string_view
  • 11. 字符串转换
  • 12. 搜索器与字符串匹配
  • 13. 文件系统
  • 14. 并行STL算法
  • 15. 标准库中的其他变化
  • 16. 移除和弃用的库特性
  • 第三部分 - 更多示例和用例
  • 17. 使用std::optional和std::variant进行重构
  • 18. 使用[[nodiscard]]强制执行代码契约
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
    • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
      • 问题
      • C++17之前的做法
      • 使用if constexpr
      • 总结
  • 20. 如何实现CSV读取器的并行化
目录

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
18. 使用[[nodiscard]]强制执行代码契约
20. 如何实现CSV读取器的并行化

← 18. 使用[[nodiscard]]强制执行代码契约 20. 如何实现CSV读取器的并行化→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式