C++ 模板:以简御繁-5/5
英文原文:https://victor-istomin.github.io/c-with-crosses/posts/templates-are-easy/
C++ 模板:以简御繁-1/5
C++ 模板:以简御繁-2/5
C++ 模板:以简御繁-3/5
C++ 模板:以简御繁-4/5
C++ 模板:以简御繁-5/5
10. 可变参数模板
还有一个主题,如果开发者谨慎使用,可以从中受益匪浅。过去,我们有一些函数,它们的参数数量是可变的。然而,现在有一组类似 printf 的函数,开发者可以传入可变数量的参数,编译器会尽力避免他们犯下许多错误。
例如,虚幻引擎将格式字符串检查合并到自定义预处理器中,而 std::format 或 fmt 为我们提供了一个有点神秘但比运行时的未定义行为要好得多的错误消息。
既然如此,为什么不为 makeString("xs: ", xs, "; and the double is: ", PI) 提供一个可变参数模板呢?至少,这是一个不错的练习。
10.1 基本语法和注意事项
template <typename... Args>
constexpr int uselessCount(Args&&... args)
{return sizeof...(args);
}int main(int argc, char**)
{return uselessCount(1,2,3) + uselessCount();
}
让我逐行描述这部杰作
第 1 行使用显而易见的语法声明了一个参数包。参数包是一个模板参数,它接受零个或多个模板参数。
第 2 行:“Args&&… args”是对包的转发引用。第 4 行:一个特殊的 sizeof... 函数,它在编译时被评估为包的参数数量。
第 9 行:一种显而易见的使用方法。
第 10 行可能不太显而易见,但仍然正确:提供了一个零个参数的包。
10.1.1 参数包扩展和递归函数
函数参数除了包含参数包之外,还可以包含一个常规参数。对于递归函数来说,它提供了一种便捷的方法,可以在参数包为空时中断递归。考虑以下伪代码中的 makeString(T&& first, Rest&&... rest) :
Rest 包含参数:return makeString(first) + makeString(rest...); Rest 是一个空参数包:return makeString(first);
虽然上面的伪代码演示了这个想法,但它并不适用于我们的代码:makeString(T&& first, Rest&&... rest) 接受一个或多个参数。由于我们已经有一组单参数实现,因此空包调用会产生歧义。解决方案是限制可变参数重载至少接受两个参数。
这是以前的一种做法:递归函数 makeString(First&& first, Second&& second, Rest&&... rest) 接受至少两个参数以及一个可选的可变参数包。它将第一个参数转换为字符串,然后递归调用 makeString(second, rest...)。一旦 rest... 包为空,它将扩展为非递归的 makeString(second) 调用。
// makeString.hpp
// ... single-parameter implementations cut from the listing ...template <typename First, typename Second, typename... Rest>
std::string makeString(First&& first, Second&& second, Rest&&... rest)
{return makeString(std::forward<First>(first)) + makeString(std::forward<Second>(second), std::forward<Rest>(rest)...);
}// main.cpp
// ... stripped ...
std::cout << makeString("a ", std::string_view("variadic " ), std::string("with a double: "), 3.14)<< std::endl;
第 8 行向读者介绍了参数包扩展:编译器将表达式 (pack)... 扩展为零个或多个表达式 (参数) 模式的逗号分隔列表。例如,std::forward<Rest>(rest)... 扩展为:
如果 Rest 没有参数:扩展为空,忽略第 8 行的 std::forward<Rest>(rest)... 表达式。
如果 Rest 有一个参数:则添加 std::forward<Rest_0>(rest_0),从而产生类似 makeString(std::forward<Second>(second), std::forward<Rest_0>(rest_0)); 的调用。
如果 Rest 有 N 个参数,则每个参数都使用封闭表达式单独扩展:std::forward<Rest_0>(rest_0), std::forward<Rest_1>(rest_1), ..., std::forward<Rest_N>(rest_N)。综上所述,第 8 行 makeString 调用的参数数量取决于参数包的大小。对于每个添加的参数,整个 std::forward<Rest_#>(rest_#) 表达式都会被“复制粘贴”。需要注意的是,这种行为适用于任何类型的打包表达式,而不仅仅是 std::forward,因此 f(g(pack)...) 将被相应地扩展为 f(g(pack_0), g(pack_1), ..., g(pack_N))。
鉴于此,编译、运行并享受!
a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
Hello, world!!1
two ; non-copyables
C{"a container with its own to_string()"}
a variadic with a double: 3.140000
10.1.2 受限版本
但是,我们可以进一步改进。与其使用 Second&& 第二个参数来避免重载冲突,不如通过要求 sizeof...(Rest) > 0 来利用约束:
template <typename First, typename... Rest>requires (sizeof...(Rest) > 0)
std::string makeString(First&& first, Rest&&... rest)
{return makeString(std::forward<First>(first)) + makeString(std::forward<Rest>(rest)...);
}
10.1.3 折叠表达式
嗯,从 C 17 开始,我们就有了折叠表达式。有了参数包 pack 和一个一元或二元运算“x”,我们就可以把 (pack x ...) 展开为 (pack_0 x (pack_1 x pack_2)),依此类推。折叠表达式有多种选择,包括左结合折叠 (... pack x)(左边带点)、右结合折叠(右边带点)、一元或二元运算选项、空包的初始化变量等等。
为了使本文篇幅合理,我大胆地将读者转发到 Fluent C++ 博客以获取深入的详细信息:第 1 部分 - 基础知识和第 2 部分 - 高级用法。
为了使本文篇幅合理,我大胆地将读者转发到 Fluent C++ 博客以获取深入的详细信息:第 1 部分 - 基础知识和第 2 部分 - 高级用法。
当我们处理字符串时,我期望左结合折叠运算符 = 比使用运算符具有更好的性能,因为后者会在涉及多个字符串的情况下生成临时变量来保存中间结果:return ((a b) c) d 除了 a、b、c 之外还会产生 3 个临时变量,而 return ((a = b) = c) = d; 会避免它们。
template <typename... Pack>requires (sizeof...(Pack) > 1)
std::string makeString(Pack&&... pack)
{return (... += makeString(std::forward<Pack>(pack)));
}
最后,我想总结一下:
模板函数可以(或许应该)受到限制,以便编译器尽早失败,并在误用的情况下提供简洁的错误上下文。即使它还没有重载。
当概念不可用时,SFINAE 是一个合理的后备方案。
使用 static_assert 和故意的编译失败来深入了解模板扩展,以防误解。
完美转发是我们的朋友:它可能提供更好的性能,并放宽对参数的“可复制”要求。
……但不适用于琐碎类型。有时,通过复制传递比通过引用传递更便宜。
可变参数模板是连续处理多个参数的便捷方法。折叠表达式可能更好。
11. 最终的代码
// makeString.hpp
#pragma once
#include <string>
#include <type_traits>
#include <concepts>template <typename T>
concept IsContainer = requires (T&& container) { std::begin(container); };// IsString is more constrained than IsContainer, so it will have a priority wherever possible
template <typename T>
concept IsString = IsContainer<T> && std::constructible_from<std::string, T>;template <typename T>
concept HasStdConversion = requires (T number) { std::to_string(number); };template <typename T>
concept HasToString = requires (T&& object)
{ {object.to_string()} -> std::convertible_to<std::string>;
};template <HasStdConversion Numeric>
std::string makeString(Numeric number)
{return std::to_string(number);
}template <HasToString Object>
std::string makeString(Object&& object)
{return std::forward<Object>(object).to_string();
}template <IsString String>
std::string makeString(String&& s)
{return std::string(std::forward<String>(s));
}template <IsContainer Iterable>
std::string makeString(Iterable&& iterable)
{std::string result;for (auto&& i : iterable){if (!result.empty())result += ';';// a constexpr if, so the compiler can omit unused branch and// allow non-copyable types usageif constexpr (std::is_rvalue_reference_v<decltype(iterable)>)result += makeString(std::move(i));else result += makeString(i);}return result;
}template <typename... Pack>requires (sizeof...(Pack) > 1)
std::string makeString(Pack&&... pack)
{return (... += makeString(std::forward<Pack>(pack)));
}
// main.cpp
#include <iostream>
#include <vector>
#include <set>
#include "makeString.hpp"struct A
{std::string to_string() const { return "A"; }
};struct B
{int m_i = 0;std::string to_string() const { return "B{" + std::to_string(m_i) + "}"; }
};struct NonCopyable
{std::string m_s;NonCopyable(const char* s) : m_s(s) {}NonCopyable(NonCopyable&&) = default;NonCopyable(const NonCopyable&) = delete;std::string to_string() const & { return m_s; }std::string&& to_string() && { return std::move(m_s); }
};struct C
{std::string m_string;auto begin() const { return std::begin(m_string); }auto begin() { return std::begin(m_string); }auto end() const { return std::end(m_string); }auto end() { return std::end(m_string); }std::string to_string() const { return "C{\"" + m_string + "\"}"; }
};template <typename Container>requires IsContainer<Container> && HasToString<Container>
std::string makeString(Container&& c)
{return std::forward<Container>(c).to_string();
}int main()
{A a;B b = {1};const std::vector<int> xs = {1, 2, 3};const std::set<float> ys = {4, 5, 6};const double zs[] = {7, 8, 9};std::cout << "a: " << makeString(a) << "; b: " << makeString(b) << "; pi: " << makeString(3.1415926) << std::endl<< "xs: " << makeString(xs) << "; ys: " << makeString(ys) << "; zs: " << makeString(zs)<< std::endl;std::cout << makeString("Hello, ") << makeString(std::string_view("world")) << makeString(std::string("!!1")) << std::endl;auto makeVector = [](){ std::vector<NonCopyable> v;v.emplace_back("two ");v.emplace_back(" non-copyables");return v; };std::cout << makeString(makeVector())<< std::endl<< makeString( C { "a container with its own to_string()" } )<< std::endl;std::cout << makeString("a ", std::string_view("variadic "), std::string("with a double: "), 3.14)<< std::endl;
}
输出为:
a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
Hello, world!!1
two ; non-copyables
C{"a container with its own to_string()"}
a variadic with a double: 3.140000
参考文献:
- CMake (Wikipedia)
- cppreference: Template specialization
- Cpp Core Guidelines: pass cheaply-copied types by value
- Arthur O’Dwyer’s blog - pass string_view by value
- cppreference: Function template
- Overload resolution of function template calls
- cppreference: Substitution Failure Is Not An Error
- cppreference: Integer Promotions
- cppreference: Type Traits
- cppreference: Metaprogramming library
- isocpp blog on universal (or forwarding) references
- cppreference: member functions
- cppreference: copy elision
- me: Practical usage of ref-qualified member function overloading
- cppreference: Constraints and concepts
- cppreference: requires clause
- Andrzej’s C++ blog - ordering by constraints
- Andrzej’s C++ blog - conjunctions, disjunctions (Requires-clause)
- fmt on Github, in case your standard library has no std::format
- cppreference: parameter pack
- cppreference: fold expressions
- Fluent C++: C++ Fold Expressions 101
- Fluent C++: What C++ Fold Expressions Can Bring to Your Code