C++ 模板:以简御繁-2/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

 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 也是根据参数构造的,即使有一个临时变量可以完美地转发给构造函数。这就引出了下一章。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值