Chapter 7 Concepts and Generic Programming – A Tour of C++

7.2 Concepts(C++20)

请参考一下上一章中的sum()函数:

template<typename Seq, typename Num>
Num sum(Seq s, Num v)
{
    for(auto x : s)
        v += x;

    return v;
}

该函数能够被任何支持begin()与end()的数据结构所调用。例如标准库中的vector,list与map。此外该类型的数据结构必须能够进行加法运算。例如int,doubble,Matrix。因此我们可以说sum()算法在两个方面是generic的:用于存储元素的数据结构的类型以及元素的类型。

所以,sum()的第一个template参数需要是某种sequence序列,而第二个template参数需要是某种数字。我们将这种要求称为concepts。

ISO C++还未支持concepts,但是ISO Technical Specification已经准许这一设定。

7.2.1 Use of Concepts

大多数template参数需要符合编译template的要求并且保证生成的代码能够正常运作。也就是说,大多数template必须是受限制的template。typename可以当作是限制最少的template参数,其只需要参数是一种类型即可。通常我们能够做得更好,因此对函数sum()进行了一定的改进。

template<Sequence Seq, Number Num>
Num sum(Seq s, Num v)
{
    for(auto x : s)
        v += x;

    return v;
}

这使得代码更为清晰。当我们定义了Sequence与Number这两个concept之后,编译器只要通过接口就能拒绝错误的调用,并不需要接入函数的实现。不过上述sum()接口并不完整,我们还能够将Sequnce的元素加入到Number中。

template<Sequence Seq, Number Num>
    requires Arithmetic<Value_type<Seq>, Num>
Num sum(Seq s, Num n);

上述代码中的Value_type表示Sequence中的元素的类型。Arithmetic也是一种concept,其要求传入的两个参数能够进行数学运算。通过这一限定,我们将以代码的形式展示我们的意图而不是注释。此外,错误的template会在编译时发生报错。还需要补充一点的是,上述代码形式是require的一种简写。

// 完整形式
template<typename Seq, typename Num>
    requires Sequence<Seq> && Number<Num> && Arithmetic<Value_type<Seq>, Num>
Num sum(Seq s, Num n);

// 组合形式
template<Sequence Seq, Arithmetic<Value_type<Seq>>Num>
Num sum(Seq, Num n);

7.2.2 Concept-based Overloading

当我们为接口设定了template,之后就能基于template的属性进行重载,其类似于函数的重载。

template<Forward_iterator Iter>
void advance(Iter p, int n)
{
    for(--n)
        ++p;
}

template<Random_access_iterator Iter, int n>
void advance(Iter p, int n)
{
    p+=n;
}

void user(vector<int>::iterator vip, list<string>::iterator lsp)
{
    advance(vip, 10); // 调用快速advance
    advance(lsp, 10); // 调用慢速advance
}

上述代码中,编译器将会选择符合template参数要求的函数。显然,标准库中的list只提供了向前迭代器,而vector则提供了随机读取的迭代器。这一技术类似于函数的重载,其由编译器处理,因此不会产生运行时的开销。如果编译器无法找到匹配的选择,那么会发生报错。

7.2.3 Valid Code

template参数是否符合某一标准,其实就是该类型的template参数是否能运用于某些表达式。我们可以使用requires来检测某一表达式是否有效。

template<Forward_iterator Iter, int n>
    requires requires(Iter p, int i) { p[i]; p+i; }
void advance(Iter p, int n)
{
    p+=n;
}

代码中连续出现的两个requires,第一个表示requirements法则的开始,第二个表示requires表达式。如果requires的表达式有效,那么其为true否则为false。

7.2.4 Definition of Concepts

我们可以在标准库中发现非常游泳的concepts,例如Sequence与Arithmetic。不过我们也可以自己定义见得concepts。concept是由编译器来检测某一类型能否被使用。

template<typename T, typename T2=T>
concept Equality_comparable = 
    requires (T a, T2 b) 
    {
        { a == b } -> bool;
        { a != b } -> bool;
    };

我们可以使用Equality_comparable来保证两种类型能够使用相同或者不相同进行比较。其中的typename T2=T类似于参数中的默认值,其表示,如果我们不限制第二个template参数,那么它就是T。我们也可以使concept更为复杂。

template<typename S>
concept Sequence = 
    requires(S a)
    {
        typename Value_type<S>; // S必须拥有值类型
        typename Iterator_type<S>; // S必须拥有迭代器类型
        { begin(a) } -> Iterator_type<S>; // begin(a)必须返回一个迭代器
        { end(a) } -> Iterator_type<S>; // end(a)必须返回一个迭代器

        requires Same_type<Value_type<S>, Value_type<Iterator_type<S>>>;
        requires Input_iterator<Iterator_type<S>>;
    };

7.4 Variadic Templates

我们可以通过template定义一个有任意数量,任意类型参数的函数。这一种template就是所谓的variadic template。

void user()
{
    print("first: ", 1, 2.2, "hello\n"s);
    // first: 1 2.2 hello

    print("\nsecond: ", 0.2, 'c', "yuck!"s, 0, 1, 2, '\n');
    // second: 0.2 c yuck! 0 1 2
}

void print()
{
    // 如果没有参数,那么什么都不做
}

template<typename T, typename ... Tail>
void print(T head, Tail ... tail)
{
    cout << head << ' ';
    print(tail...);
}

一般来说,如果要实现一个variadic template,那么我们需要将第一个参数与之后的参数进行区分,之后不断通过variadic template来调用tail参数。上述代码中的typename …意味着Tail是一个sequnce类型。而Tail…意味着tail就是一组有Tail中的元素所组成的sequence。通过…声明的参数被称为一个parameter pack。

函数print()会将会吧参数分为head(第一个元素)与tai(其余元素)。head将被print之后再调用tail的print。最后tail将会变空,此时我们就需要没有参数的print()版本进行处理。如果我们不想要无参数版本的print,可以使用编译器版本的if。

template<typename T, typename ... Tail>
void print(T head, Tail... tail)
{
    cout << head << ''; if constexpr(sizeof...(tail)> 0)
        print(tail...);
}

7.4.1 Fold Expressions

为了简化variadic template的实现,c++17提供了下列选择。

template<Number... T>
int sunm(T... v)
{
    return (v + ... + 0); // 累加v中的所有元素,并以0作为起始
}

函数sum()能够累加任意类型,任意数量的参数。函数中的(v+…+0)意味着v中所有的元素将会与初始值0开始累加。第一个相加的元素是“最右侧”的元素(也就是索引值最大的元素):(v[0]+(v[1]+(v[2]+(v[3]+(v[4]+0)))))。而最右侧的0被称为right fold。同样,我们也可以使用一个left fold。

template<typename... T>
int sum2(T... v)
{
    return (0 + ... + v);
}

Fold是非常有用的抽象工具。在c++中,fold表达式只能用于简化variadic template的实现。fold并不一定要用来处理数字上的运算。

template<typename ...T>
void print(T&&... args)
{
    (std::cout << ... << args) << '\n'; // 打印所有的参数
}

print("Hello!"s, ' ', "World ", 2017);
template<typename Res, typename... Ts>
vector<Res> to_vector(Ts&&... ts)
{
    vector<Res> res;
    (res.push_back(ts) ...);
    return res;
}

auto x = to_vector<double>(1,2,4.5,'a');

7.6 Advice

[01] template提供了一种编译器的泛型编程。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据