英文原文:https://victor-istomin.github.io/c-with-crosses/posts/templates-are-easy/
6. makeString() 中对集合的支持
让我们升级 makeString() 函数,使其支持通用容器,提升编程乐趣。首先,修改一下测试代码,添加一些用例:
#include <iostream>
#include <vector>
#include <set>
#include <vector>
#include <set>
#include<string>
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) + "}"; }
};
template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
return object.to_string();
}
template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
return std::to_string(value);
}
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;
}
当然,上面的代码缺少必要的模板。我喜欢不假思索地直接开始编码,但在这种情况下,我们得巧妙一些。集合可以是向量、集合或 C 数组,但在一般情况下,它是可迭代的。我们可以对可迭代对象使用 std::begin。因此,我们需要一个模板,它能接受与 std::begin 兼容的对象并对其进行迭代。
来,开始敲代码吧。
// makeString.hpp
// ...
template <typename Iterable>
auto makeString(const Iterable& iterable)
-> decltype(makeString(*std::begin(iterable))) // (1)
{
std::string result;
for (const auto& i : iterable)
{
if (!result.empty())
result += ';';
result += makeString(i);
}
return result;
}
为了以防万一,快速说明一下(1):这是一个模板,它可以接受传递给 std::begin 的内容,并且 makeString返回的类型与该集合第一个元素的类型相同。
全部代码如下
#include <iostream>
#include <vector>
#include <set>
#include<string>
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) + "}"; }
};
template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
return object.to_string();
}
template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
return std::to_string(value);
}
template <typename Iterable>
auto makeString(const Iterable& iterable)
-> decltype(makeString(*std::begin(iterable))) // (1)
{
std::string result;
for (const auto& i : iterable)
{
if (!result.empty())
result += ';';
result += makeString(i);
}
return result;
}
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;
}
编译、运行,一切顺利
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
然而,该代码有一个陷阱,我们将在下一节中探讨
7. makeString()支持字符串类型的参数
以防万一,为什么不呢?这或许能简化其他模板中 makeString() 的用法。我们先添加一些测试用例。
// main.cpp
// ...
std::cout << makeString("Hello, ")
<< makeString(std::string_view("world"))
<< makeString(std::string("!!1"))
<< std::endl;
#include <iostream>
#include <vector>
#include <set>
#include<string>
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) + "}"; }
};
template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
return object.to_string();
}
template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
return std::to_string(value);
}
template <typename Iterable>
auto makeString(const Iterable& iterable)
-> decltype(makeString(*std::begin(iterable))) // (1)
{
std::string result;
for (const auto& i : iterable)
{
if (!result.empty())
result += ';';
result += makeString(i);
}
return result;
}
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)<< makeString("Hello, ")
<< makeString(std::string_view("world"))
<< makeString(std::string("!!1"))
<< 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
72;101;108;108;111;44;32;0119;111;114;108;10033;33;49
字符串是一串字符的可迭代容器。我们还从 cppreference 的“Integer Promotions ”章节了解到,当没有字符重载时,编译器会将字符提升为整数。为了解决这个问题,我们需要从匹配的字符串类型中排除容器模板。
此外,将 makeString(char) 标记为已删除的函数以永远避免陷阱也是合理的:
std::string makeString(char c) = delete;
[build] makeString.hpp:62:6: note: candidate template ignored: substitution failure
[with Iterable = const char (&)[8]]: call to deleted function 'makeString'
[build] auto makeString(Iterable&& iterable) -> decltype(makeString(*std::begin(iterable)))
[build] ^ ~~~~~~~~~~
这是提及类型特征和 std::enable_if 的好时机。
7.1 Type traits and enable_if:一种针对特征或条件专门化模板的方法
一种解决方案是限制容器的 makeString 方法,使其在字符串替换时失败,然后我们重新写入一个新的字符串函数。假设“string”是 std::string、std::string_view、char* 和 const char*。让我们用 C++ 来表达:
namespace traits
{
// generic type is not a string...
template <typename AnyType> inline constexpr bool isString = false;
// ... but these types are strings
template <> inline constexpr bool isString<std::string> = true;
template <> inline constexpr bool isString<std::string_view> = true;
template <> inline constexpr bool isString<char*> = true;
template <> inline constexpr bool isString<const char*> = true;
}
上面的模板我们称之为变量模板,变量的值默认为false,然后在string相关的特化类型下为true。
但是等等,const string、const string_view、const char* const 又是什么呢?而且还有一个 inline 关键字。幸运的是,我们可以通过使用一种类型映射来避免大量的复制粘贴:T -> T,const T -> T。例如,我们可以编写如下代码:
namespace traits
{
namespace impl // a "private" implementation
{
// generic type is not a string...
template <typename AnyType>
inline constexpr bool isString = false;
// ... but these types are strings
template <> inline constexpr bool isString<std::string> = true;
template <> inline constexpr bool isString<std::string_view> = true;
template <> inline constexpr bool isString<char*> = true;
template <> inline constexpr bool isString<const char*> = true;
}
namespace Wheel
{
template <typename T> struct remove_cv { using type = T; };
template <typename T> struct remove_cv<const T> { using type = T; };
template <typename T> struct remove_cv<volatile T> { using type = T; };
template <typename T> struct remove_cv<const volatile T> { using type = T; };
// shortcut to remove_cv<T>::type, saves a bit of stamina on fingers
template <typename T> using remove_cv_t = typename remove_cv<T>::type;
// enable_if only defined as a specialization enable_if<true, T>
// thus failing substitution on any code that uses enable_if<false, T>;
template <bool Condition, typename T> struct enable_if;
template <typename T> struct enable_if<true, T> { using type = T; };
// another shortcut
template <bool B, class T> using enable_if_t = typename enable_if<B,T>::type;
}
// traits::isString<T> = traits::impl::isString<T>, but with remove_cv_t on T:
template <typename T>
inline constexpr bool isString = impl::isString<
Wheel::remove_cv_t<T>
>;
}
然而,我们已经养成了提前查看 cppreference.com 的习惯,所以我们将使用 std::remove_cv_t 和 std::enable_if_t,而不是重新发明轮子:
#pragma once
#include <string>
#include <type_traits>
#include<vector>
#include <set>
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) + "}"; }
};
namespace traits
{
namespace impl // a "private" implementation
{
// generic type is not a string...
template <typename AnyType>
inline constexpr bool isString = false;
// ... but these types are strings
template <> inline constexpr bool isString<std::string> = true;
template <> inline constexpr bool isString<std::string_view> = true;
template <> inline constexpr bool isString<char *> = true;
template <> inline constexpr bool isString<const char *> = true;
}
// isString<T> = impl::isString<T>, but with dropped const/volatile on T:
template <typename T>
inline constexpr bool isString = impl::isString<std::remove_cv_t<T>>;
}
template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
return object.to_string();
}
template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
return std::to_string(value);
}
// will fail substitution if `traits::isString<Iterable>`
// or when can't evaluate the type of makeString(*begin(container))
template <typename Iterable>
auto makeString(const Iterable& iterable)
-> std::enable_if_t< !traits::isString<Iterable>,
decltype(makeString(*std::begin(iterable))) >
{
std::string result;
for (const auto& i : iterable)
{
if (!result.empty())
result += ';';
result += makeString(i);
}
return result;
}
// will fail substituition if `!traits::isString<String>`
template <typename String>
auto makeString(const String& s)
-> std::enable_if_t< traits::isString<String>,
std::string >
{
return std::string(s);
}
// main.cpp
// ...
int main(int argc, char const *argv[])
{
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;
return 0;
}
现在编译、运行、如愿以偿:
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
72;101;108;108;111;44;32;0world!!1
对“Hello "字符串来说,由于在数组的的末尾存储了空字符,所以字符数组的大小比单词Hello "的字符数多1, 这就是为啥world前面有个0.
7.2 了解模板扩展后的内容
根据输出,“Hello, ” 被当作一个容器处理。使用一些技巧可以更容易理解发生了什么。至少有几种方法:将一个最小可编译示例上传到编译器分析工具,或者故意制造一个带有描述性错误消息的失败。
7.2.1 C++ 见解
代码编译成功后,我们就可以将其上传到 cppinsights.io。这里有一个略微精简的代码链接。
下面是生成的输出的一部分:
namespace traits
{
namespace impl
{
// ...
template<>
inline constexpr const bool isString<char *> = true;
template<>
inline constexpr const bool isString<const char *> = true;
template<>
inline constexpr const bool isString<char[8]> = false;
template<>
inline constexpr const bool isString<char> = false;
}
// ...
}
// ...
/* First instantiated from: insights.cpp:59 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
std::basic_string<char> makeString<char[8]>(const char (&iterable)[8])
{
// ...
}
#endif
从上面的输出可以明显看出,makeString("Hello, ") 实际上是 makeString<char[8]>(/*reference-to-char[8]*/),而 impl::isString 没有这样的重载,因此需要使用泛型:impl::isString<char[8]> = false;
7.2.2 故意编译失败
然而,可能会有这种情况发生,代码可能不应该提交到任何其它地方,或者无法编译。在这种情况下,故意制造的编译错误可能会让人知晓到推断出的类型和值。deleted 函数在调用时会提供良好的错误上下文,而 undefined 结构体在实例化时则会让我们更好第理解编译错误:
// ...
template <typename... Args> bool fail_function(Args&&... args) = delete;
template <bool Value> struct fail_struct; // never defined
// ...
static bool test1 = fail_function("Hello, "); // (1)
static fail_struct< traits::isString<decltype("Hello, ")> > test2 = {}; // (2)
编译输出如下:
error: use of deleted function ‘bool fail_function(Args&& ...)
[with Args = {const char (&)[8]}]’
40 | static bool test1 = fail_function("Hello, "); // (1)
| ~~~~~~~~~~~~~^~~~~~~~~~~
error: variable ‘fail_struct<false> test2’ has initializer but incomplete type
41 | static fail_struct< traits::isString<decltype("Hello, ")> > test2 = {}; // (2)
| ^~~~~
让我们对错误信息进行抽丝剥茧
- 在 fail_function("Hello, ") 中,参数类型是对 const char[8] 的引用。尽管它可以隐式转换为 const char*,但它与 const char* 有所不同。
- 在 fail_struct<traits::isString<...> > 变量中,参数为 false,因此traits::isString<...> 的结果为 false。
7.3 最后,makeString 接受一个字符串并按预期工作
以防有人想要解决 isString<char[N]> 特征
namespace impl
{
// ...
template <size_t N>
inline constexpr bool isString<char[N]> = true;
// ...
}
在之前的章节中,我们发明了一些轮子,并理解了类型特征背后的想法。现在是时候重新思考它了。有了标准库,我们可以更好地处理字符串。例如,如果可以从一个类型显式地构造出 std::string ,我们就认为该类型是字符串。就是这样。
namespace traits
{
template <typename T>
inline constexpr bool isString = std::is_constructible_v<std::string, T>;
}
将各个部分组合在一起,编译并运行。
// main.cpp
#include <iostream>
#include <vector>
#include <set>
#include <string>
#include <type_traits>
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) + "}"; }
};
namespace traits
{
template <typename T>
inline constexpr bool isString = std::is_constructible_v<std::string, T>;
}
template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
return object.to_string();
}
template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
return std::to_string(value);
}
// will fail substitution if `traits::isString<Iterable>`
// or when can't evaluate the type of makeString(*begin(container))
template <typename Iterable>
auto makeString(const Iterable& iterable)
-> std::enable_if_t< !traits::isString<Iterable>,
decltype(makeString(*std::begin(iterable))) >
{
std::string result;
for (const auto& i : iterable)
{
if (!result.empty())
result += ';';
result += makeString(i);
}
return result;
}
// will fail substituition if `!traits::isString<String>`
template <typename String>
auto makeString(const String& s)
-> std::enable_if_t< traits::isString<String>,
std::string >
{
return std::string(s);
}
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;
}
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
现在评判一下:这仍然是一个好的开始!至少,问题在于 std::string 也是根据参数构造的,即使有一个临时变量可以完美地转发给构造函数。这就引出了下一章。

被折叠的 条评论
为什么被折叠?



