C++元编程

元编程是C++的一个重点和难点,标准库中有非常多的内容都是通过模板实现的。

C++11标准以来,C++在泛型编程方面引入了许多新的特性。

type traits

traits 是C++ STL中的重要组成成分。type traits 的实现原理是 SFINAE,这里主要是介绍使用。

数值部分

  1. 判断是否是整数:std::is_integral<T>::value
  2. 判断是否有符号:std::numeric_limits<T>::is_signed

迭代器部分

判断迭代器种类以及内容:std::iterator_traits。这个trait实现很有意思,为了兼容指针和常指针这两个最原始的“迭代器”,这里使用了SFINAE

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
template<class _Iter>
struct _Iterator_traits_base<_Iter, void_t< // 这里的void_t使得不具有下面五个成员的两种指针失配
typename _Iter::iterator_category,
typename _Iter::value_type,
typename _Iter::difference_type,
typename _Iter::pointer,
typename _Iter::reference
> >
{ // defined if _Iter::* types exist
typedef typename _Iter::iterator_category iterator_category;
typedef typename _Iter::value_type value_type;
typedef typename _Iter::difference_type difference_type;

typedef typename _Iter::pointer pointer;
typedef typename _Iter::reference reference;
};

template<class _Iter>
struct iterator_traits
: _Iterator_traits_base<_Iter>
{ // get traits from iterator _Iter, if possible
};

template<class _Ty>
struct iterator_traits<_Ty *>
{ // get traits from pointer
typedef random_access_iterator_tag iterator_category;
typedef _Ty value_type;
typedef ptrdiff_t difference_type;

typedef _Ty *pointer;
typedef _Ty& reference;
};

// 注意 const _Ty *要单独拿出来偏特化,如果用上面的偏特化的话,我们得到的是const _Ty而不是_Ty
template<class _Ty>
struct iterator_traits<const _Ty *>
{ // get traits from const pointer
typedef random_access_iterator_tag iterator_category;
typedef _Ty value_type;
typedef ptrdiff_t difference_type;

typedef const _Ty *pointer;
typedef const _Ty& reference;
};

对象行为部分

C++ 新标准中使用了 is_...able<> 函数代替了之前的 has_... 函数

reference_wrapper

显然 std::vector 是不能存放T &的,如果不希望放指针进去,可以使用std::reference_wrapper,不过这东西用起来也并不舒服,例如C++不能重载operator.,虽然最近有一些草案正在提到这一点,所以必须得get()一下。
std::reference_wrapper常用于std::bindstd::thread等函数/类上。它们为了保证传入的参数在自己的生命周期中是持续存在的,决定始终通过传值的方式接受参数,即始终拷贝一份,因此此时如果要传递引用,就得使用std::ref包装一下了。

operator重载

operator=operator()operator[]operator->operator T不能作为non-member function,这是因为担心与自动合成的operator=出现行为不一致的问题。其实还可以更深入地考虑这个问题,例如对于比较大小的运算符operator<

1
2
3
4
5
6
7
8
9
10
11
12
struct A;
struct B;
struct A {
operator<(const B & b){
return true;
}
};
struct B {
operator<(const A & b){
return true;
}
};

这显然是不符合逻辑的

特化一些其他结构

特化using

与函数相同,C++中的using也不能直接特化,如

1
2
3
4
5
template <typename T>
using wrap<T> = T;

template <>
using wrap<int> = int;

必须需要借助类来workaround

1
2
3
4
template <typename T>
struct wrap {
using type = T;
}

函数模板的特化与函数的重载

为了实现函数的“多态”,常有借助模板类偏特化和重载两种方式。

借助重载实现函数的“偏特化”

虽然函数模板不能偏特化,但函数本身就有重载,因此我们可以在每个不同的函数末尾加上一个tag来达到根据某些traits进行“分类讨论“的效果。例如最常用的std::true_typestd::false_type。在STL的实现中使用了很多这样的_tag来控制行为,例如控制std::advance()函数在接受五种不同迭代器时的更新行为,或者原子库中使用tag标记六种内存模型。即使当我们要“偏特化”的是非类型模板参数时,也可以直接利用重载而不是“偏特化”,这时候我们可以在类里面用enum或者static constexpr int将这个值封装起来,也可以直接借助于标准库的std::condition等函数。在下面的例子中,我们需求模板函数的行为根据std::is_same<T, C>::value进行分类讨论。

1
template<typename T, typename U> when_eq(T x, U y); // 1

在没有constexpr if的情况下,用函数重载来做是一种直截了当的方案。但是重载是对类型而言的,而if只能判断true/false值,非类型模板参数又不能使用整型以外的类型。所以必须要有个机制来将整型值包装成类型,这里可以借助标准库提供的std::integral_constant<typename T, T val>,或者也可以自己实现一个Int2Type或者Int2Type2。这个思路可以解决很多问题,例如对成员函数的偏特化

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
26
27
28
29
30
31
template <int I>
struct Int2Type
{
enum { value = I };
};
template <int I>
struct Int2Type2
{
static const /* or constexpr */ int value = I;
};

struct X{};
struct Y{};

template <typename T, typename U>
void when_eq(T x, U y, Int2Type<1>) {
puts("eq");
}
template <typename T, typename U>
void when_eq(T x, U y, Int2Type<0>) {
puts("neq");
}
template <typename T, typename U>
void when_eq(T x, U y) {
when_eq(x, y, Int2Type<std::is_same<T, U>::value>());
}

int main(){
when_eq(X(), Y()); // neq
when_eq(X(), X()); // eq
}

这里特别提一下enum的性质,根据标准在使用enum E {V1, V2}的值V1V2时是不需要加上E::限定符的。此外其具体类型可以使用std::underlying_type获得。

借助类模板实现偏特化

还可以使用类模板“偏特化”函数,如果需要上下文,那么可以重载operator()的方式实现一个仿函数,如果不需要上下文,可以在里面定义一个static函数。
如果一定需要偏特化,考虑在一个偏特化类中实现static函数。这又带来一个新的问题,考虑要偏特化一个类中成员函数,如果偏特化类,那其他的成员函数也要重复实现一遍,显得很麻烦,对于这个问题,可以参考这里的说明

普通函数、函数模板和全特化模板函数之间的决议

在这一章节中,我们将讨论重载的普通函数、函数模板和其特化函数之间的决议顺序。
模板函数一旦全特化,它就和普通函数一样成为一个strong symbol,我们应当使用inlinestaticextern关键字防止重复定义。
但是对于全特化的函数模板,它和不加template<>的普通函数还是有着很大区别的?根据C++ Premier中的说明

当调用从模板实例化的函数时,只有有限的类型转换可以被应用在模板实参推演过程使用的函数实参上;如果声明一个普通函数则可以考虑用所有的类型转换来转换实参,这是因为普通函数参数的类型是固定的

上面的话是说明template<>特化函数模板比普通函数的匹配要求更高,这是因为普通函数还能接受传入参数的隐式转换。在C++ Primer的16.5节中指出,在多个函数之间进行决议时总是选择最为精确的函数,当普通函数和模板函数同样适配时,优先选择普通函数

  1. 在这个例子中,aint,显然主模板0能够精确匹配,于是决议到0,输出"template"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T>
    void promotion(T x) {
    puts("template");
    }
    // #1
    void promotion(double x) {
    puts("function");
    }

    int main() {
    int a = 1;
    promotion(a); // template
    }
  2. 这个例子就是一个经典的SFINAE用法了,我们知道一个数组的大小只能是整数而不能是浮点数,所以当Tdouble时,char(*)[T()]就是ill-formed。根据SFINAE,我们不把它作为错误,而是转而寻求次优解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T, typename = char(*)[T()]>
    void promotion(T x) {
    puts("template");
    }
    // #1
    void promotion(double x) {
    puts("function");
    }

    int main() {
    int a = 1;
    promotion(a); // function
    }

但必须要考虑一个特例,也就是当有多于一个函数能精确匹配的情况,这时候优先级的顺序是普通函数、全特化、主模板。首先给出一个综合的实例,并讨论下面的几点比较简单的方面。

  1. 普通函数先于全特化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // #0
    template<typename T>
    void promotion(T x) {
    puts("template");
    }
    // #1
    void promotion(double x) {
    puts("function");
    }
    // #2
    template<>
    void promotion(double x) {
    puts("template<>");
    }
    int main() {
    promotion(1.0); // function
    }
  2. 全特化版本先于主模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T>
    void promotion(T x) {
    puts("template");
    }
    // #1
    template<>
    void promotion(double x) {
    puts("template<>");
    }

    int main() {
    promotion(1.0); // template<>
    }
  3. decay
    想一想,如果主模板要发生 decay,更不可能会被选中。因为普通函数优先于全特化,进而优先于主模板。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // #0
    template<typename T>
    void promotion(T x) {
    puts("template");
    }
    // #1
    void promotion(int & x) {
    puts("function");
    }

    int main() {
    int a = 1;
    promotion(a); // function
    }
  4. 一些类型转换的 case,可以参考综合实例

  5. 一些和 SFINAE 相关的 case,同样可以参考综合实例

上述的重载决议,可以被用在stateful constexpr 实现 counter上。

考虑可访问性

C++中的可访问性包括private、protected和public三类,可访问性是针对而不是对象而言的。
C++中重载决议是在可访问性检查之前进行的

考虑隐式类型转换

在日常编程中,我们常常把字符串直接量当做std::string使用,但其实字符串直接量的类型是const char[],这其中涉及到隐式构造函数或者隐式类型转换。这导致在进行重载决议时出现“不合常理”的情况。

有关更多隐式类型转换的内容,可以参考

普通情况

知乎的这篇问题中,我们看到Test中定义了两个成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
public:
void find(const string &first_name, const string &last_name);
void find(const string &name, bool retied);
};

void Test::find(const string &first_name, const string &last_name)
{
cout << "find 1" << endl;
}

void Test::find(const string &name, bool retied)
{
cout << "find 2" << endl;
}

现在考虑下面的调用,我们发现是调用的find 2版本的函数。这是由于const char[]被decay成了const char *,然后隐式转换成了bool

1
Test().find("a", "b");

考虑传参时decay的特殊情况

紧接着上面的讨论,我们查看下面的一段代码,这时候输出就变成true了,究其原因是因为

1
2
3
4
5
6
7
8
9
template<typename T, typename U>
bool check(T x, U y) {
return std::is_same<T, U>::value;
}
int main() {
int a = 1;
const int & b = a;
std::cout << check(int(), a) << '\n';
}

同样是类型转换问题,在文章中提到了一个特别的情况

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
int compare(const T& a, const T& b)
{
std::cout << "template" << std::endl;
return 0;
}

int compare(const char* a, const char* b)
{
std::cout << "normal" << std::endl;
return 0;
}

现在调用

1
compare("123456", "123456");

请问是调用了那个函数呢?有意思的是对于不同编译器结果还不一样。作者指出G++/clang调用了模板版本,而我使用VS2015发现调用的是普通版本。
调用模板版本的原因是T = const char [6]相比decay后的const char *更精确;但是调用普通版本的原因是数组传参时要 decay 成对应的指针。因此对于模板函数和普通函数实际上都不是精确匹配的,也就是都要经过一次类型转换。根据C++重载决议原则,如果调用具有二义性,则优先选择调用普通函数。
在本篇结尾,作者引用陈硕的观点,认为G++/clang的实现是符合标准的,但是这属于标准的bug。其原因是**模板实参推断(template argument deduction)**时,除了产生新的实例化之外,编译器只会执行两种转换:

  1. const 转换:接受 const 引用或指针的函数可以分别用非 const 对象的引用或指针来调用,无须产生新的实例化。也就是可以传一个非 const 到需要 const 参数的函数。
  2. 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针(如前文所说),函数实参当作指向函数类型的指针。

由于模板参数使用了数组的 const 引用类型,所以按照标准应该不将数组向指针进行 decay。

考虑继承

SFINAE

在C++中常常出现一些ill-formed的代码,这些代码在函数体或者其他地方出现编译器都是要报错的,SFINAE(Substitution Failure Is Not An Error)规定,当推导模板参数失败(fail)时,编译器不产生错误,而是放弃使用这个重载集推导参数。需要注意这样的替换发生在下面四种情况下:

  1. (Type SFINAE)所有用于函数的类型,包括函数的返回值与参数的类型。
  2. (Type SFINAE)所有用于模板参数的类型。
  3. (Expression SFINAE)所有用于函数类型中的表达式。
  4. (Expression SFINAE)所有用于模板形参声明中的表达式。

介绍

函数模板参数会被替换两次:

  1. 显式特化的模板参数在 template argument deduction 前被替换。
  2. 推导出的参数,和默认的参数在 template argument deduction 后被替换。

替换发生在:

  1. 在函数类型中使用的任意类型,包括返回类型和参数类型
  2. 在 template parameter declaration 中使用的所有类型
  3. 【C++11】在 function type 中用到的所有表达式
  4. 【C++11】在 template parameter declaration 中用到的所有表达式
  5. 【C++20】all expressions used in the explicit specifier

A substitution failure is any situation when the type or expression above would be ill-formed (with a required diagnostic), if written using the substituted arguments.

只有在 function type 和它的 template parameter type 和 explicit specifier 的 immediate context 中的 type 和 expression 中的 failures 才被看做 SFINAE 错误。If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors.

Type SFINAE

这些常被使用的 ill-formed 代码包括下面的形式:

  1. 试图实例化一个包展开,但这个包展开中含有多个不同长度的包

  2. 试图创建 void 的数组、引用的数组、函数的数组、抽象类类型的数组、负大小的数组或零大小的数组
    例如之前提到过的char(*)[0.5],还有char [0]void [3]这些都不是一个合法的。

  3. 试图在::左侧使用非类或非枚举的类型
    如下所示,0 这个 int 并不具有 B 这个字段,所以会匹配到第二个。

    1
    2
    3
    4
    5
    6
    7
    template<class T>
    int f(typename T::B*);

    template<class T>
    int f(T);

    int i = f<int>(0); // uses second overload
  4. 试图使用类型的不存在的成员
    比如vector<int>::abcdefg是错的,因为它肯定没有这个字段,但不止于此。主要分为下面几种类型:

    • 该类型中没有这样的成员。
    • 该类型中有这样的成员,但是类型不匹配。
    • 该类型中有这样的成员,但是它不是个模板,而我们要求它是个模板。
    • 该类型中有这样的成员,但是它是个 non-type,而我们要求它是个 nin-type。
  5. 试图在错误的地方是用类型的成员
    类型、模板、非类型如 literal 等。

  6. 试图创建指向引用的指针

  7. 试图创建到 void 的引用

  8. 试图创建指向 T 成员的指针,其中 T 不是类类型
    这个常常可被用来判断T是否是类类型,代码如下所示。
    这里C::*表示一个指向类C中某int类型成员的指针,说明了这个指针带有C的上下文,而...相当于一个兜底的东西。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<typename C> 
    bool test(int C::*){
    return true; // C是类
    }
    template<typename C>
    bool test(...){
    return false;
    }
    struct X{
    double x;
    };

    int main() {
    cout << test<int>(0)<< endl; // false
    cout << test<X>(0)<< endl; // true
    }
  9. 试图将非法类型给予非类型模板参数

    1
    2
    3
    4
    5
    6
    7
    8
    template<class T, T>
    struct S {};

    template<class T>
    int f(S<T, T()>*);

    struct X {};
    int i0 = f<X>(0);

    这个SFINAE将在后面std::void_t中被使用。

  10. 试图进行非法转换

  11. 试图创建拥有类型void的形参的函数

  12. 试图创建返回数组类型或函数类型的函数

  13. 试图创建参数类型或返回类型为抽象类的函数类型

自然而然地想到如果需要判定某个实体是否具有某种性质,我们就可以构造一种没有这种特性就会造成ill-formed的表达式或者类型,分别对应Expression SFINAE和Type SFINAE。这样我们就可以把traits的问题转化为函数重载或者模板类的偏特化的决议问题来讨论。

在函数重载使用SFINAE时,常常在定义函数时在函数最后放一个可能会ill-formed类型的指针,在调用时传一个nullptr过去。相对于遥不可及的concepts,SFINAE机制看起来更为神奇,它在不引入一个新的语法机制的情况下来实现了traits的功能,但伴随而来的是我们把Failure和Error都放在了模板实例化或者重载决议时处理,这样当Error发生时我们就不能得到清晰的错误报告。

Expression SFINAE

只介绍从 C++ 11 开始的内容。
下列的 Error 是 SFINAE error:

  1. Ill-formed expression used in a template parameter type
  2. Ill-formed expression used in the function type

如下所示,有两个 f,#1 要求返回类型是 decltype(t1 + t2),#2 要求返回类型是 X。对于 X 类型的 x1 和 x2 而言,x1 + x2 是 ill-formed,所以 SFINAE 会选择到 #
2。

1
2
3
4
5
6
7
8
9
10
11
struct X {};
struct Y { Y(X){} }; // X is convertible to Y

template<class T>
auto f(T t1, T t2) -> decltype(t1 + t2); // overload #1

X f(Y, Y); // overload #2

X x1, x2;
X x3 = f(x1, x2); // deduction fails on #1 (expression x1 + x2 is ill-formed)
// only #2 is in the overload set, and is called

偏特化中的 SFINAE

Deduction and substitution also occur while determining whether a specialization of a class or variable【C++14】 template is generated by some partial specialization or the primary template.
编译器在做出决议时,不会将这个的替换失败看做是硬错误,而是只会忽略这个对应的偏特化错误,就好像在涉及函数模板的重载中一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// primary template handles non-referenceable types:
template<class T, class = void>
struct reference_traits
{
using add_lref = T;
using add_rref = T;
};

// specialization recognizes referenceable types:
template<class T>
struct reference_traits<T, std::void_t<T&>>
{
using add_lref = T&;
using add_rref = T&&;
};

template<class T>
using add_lvalue_reference_t = typename reference_traits<T>::add_lref;

template<class T>
using add_rvalue_reference_t = typename reference_traits<T>::add_rref;

样例

enable_if

1
2
template< bool B, class T = void >
struct enable_if;

如果 B 是 true,那么 enable_if 具有一个 T 类型的成员 type,否则没有任何类型。

这个函数可以在 concept 之前简化 SFINAE 的使用。特别是用来给定一定的条件,然后通过 type traits 将某些函数从 candidate set 中删除。allowing separate function overloads or specializations based on those different type traits。

std::enable_if can be used in many forms, including:

  1. as an additional function argument (not applicable to operator overloads)
  2. as a return type (not applicable to constructors and destructors)
  3. as a class template or function template parameter

它的实现也很简单

1
2
3
4
5
template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { typedef T type; };

void_t

这个的作用是将一系列的类型都映射为 void

1
2
template< class... >
using void_t = void;

This metafunction is a convenient way to leverage SFINAE prior to C++20’s concepts, in particular for conditionally removing functions from the candidate set based on whether an expression is valid in the unevaluated context (such as operand to decltype expression), allowing to exist separate function overloads or specializations based on supported operations.

使用SFINAE判断是否存在成员

利用SFINAE的方式分为两种,通常见到的是Type SFINAE。关于Expression SFINAE,可以参考这个回答

模板偏特化是相对原型/初等/主(primary template)模板来说的,编译器首先匹配出原型,再根据原型找出合适的特化模板。例如对模板类型参数T而言,T*T&const T &等是它的一个偏特化。通过偏特化我们可以进行分类讨论。

1
2
3
4
5
6
7
8
9
10
11
// 终止条件
template<typename Cont>
bool handle_container(Cont & cont) {
puts("not a container");
};

template<typename Cont>
bool handle_container(Container<Cont> & cont) {
puts("container");
handle_container(cont[0]);
};

偏特化可以用来检查一个类中是否具有某个成员,但当我们尝试匹配成员函数,写出如下代码时,则会发现匹配失败

1
2
3
4
5
6
// https://www.zhihu.com/question/34264462
template<typename Cont, typename = void>
struct has_push_back : std::false_type {};

template<typename Cont>
struct has_push_back<Cont, std::void_t<decltype(&Cont::push_back)>> : std::true_type {};

后来查看SoF,当时以为是MSVC2015对std::void_t的支持有问题。
后来在SoF的另一篇博文上发现如果我们的push_back不是成员函数而是数据成员就可以通过。去SoF上问了一波,这是因为std::vector<int>::push_back有多个重载版本,于是应该还要匹配函数签名。因此可以显式构造vector.push_back(vector::value_type())这个表达式,看它是不是合法。

1
2
3
4
5
6
7
8
9
// https://www.zhihu.com/question/34264462/answer/58260115
template<typename Cont, typename = void>
struct has_push_back : std::false_type{};

template<typename Cont>
using ValueType = typename Cont::value_type;

template<typename Cont>
struct has_push_back<Cont, std::void_t<std::declval<Cont>().push_back(declval<ValueType<Cont>>())>> : std::true_type{};

注意到这里都是用类的偏特化来实现的,我也尝试了下使用函数搞一搞

1
2
3
4
5
6
7
8
template<typename Cont, typename = void> 
bool has_push_back() {
return false;
}
template<typename Cont, typename = std::void_t<decltype(&Cont::push_back)>>
bool has_push_back() {
return true;
}

这样会出现函数模板已经定义的错误。在cppreference中给出了说明

A common mistake is to declare two function templates that differ only in their default template arguments. This is illegal because default template arguments are not part of function template’s signature, and declaring two different function templates with the same signature is illegal.

此外在SoF 上给出了如下说明

SFINAE only works for deduced template arguments

判断模板参数是否是广义函数

目前广义函数包括函数、std::function、lambda、仿函数,其中std::is_function只能识别函数。C++17标准添加了std::is_invokable(即之前的std::is_callable),用来表示可以INVOKE的类型。关于这一部分,可以参考我的博文C++仿函数的作用实现

推导lambda的类型

根据StackOverflow上的这个答案,lambda是函数对象而不是函数,但是可以被转换(convert to)成std::function,这是因为std::function可以从所有能被调用的类型构造,也就是说这可以这么写

1
std::function<int(int)> lambda = [](int x) { return 0; };

但是类型推导是另一回事,因为lambda并不是std::function,我们只能用auto来声明一个lambda表达式,所以编译器不能通过一个lambda去推导std::function的参数。BTW,Effective Modern C++指出使用auto关键字相对于使用显式声明是比较好的。具体得来说他可以避免default initialization的变量、难以表示的类型、避免可能造成的类型截断问题。
但是,函数指针可以看做函数的入口点,它是全局的,而一个函数对象如std::function可以携带context,所以是不能转换为一个函数指针的。那我们有办法将std::function也“转换”为函数指针么?这需要一些技巧,如这个方法中定义了to_function_pointer,它会返回一个 lambda 表达式,在这个 lambda 表达式中,会调用 std::function。另一种方案是不借助lambda表达式
特别地,标准指出一个不捕获lambda表达式可以被转成一个函数指针。
在这里还需要将函数签名与函数指针区分开来,一般在使用函数签名的场合常需要考虑C linkage。

推导 F 的返回值类型

比较省事的解决方式是直接使用template<typename F>,现在的问题是如何得到F的返回值类型。
这时候可以借助std::result_of来推导typename F的返回类型,注意这里要写成decltype(lambda_func)(),而不是decltype(lambda_func)

1
2
3
4
auto lambda_func = [](){
return 1 + 2;
};
using lambda_func_t = std::result_of<decltype(lambda_func)()>::type;

此外,经测试可以推导函数对象的返回类型,不能推导C原生函数的返回类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int native_func(int a) {
return 0;
}
struct object_func {
int operator()(int a) {
return 0;
}
};
template<typename F>
auto call_func(F f)-> std::result_of_t<F(int)> {
return 0;
}
int main() {
typename std::result_of<object_func(int)>::type x; // Yes
typename std::result_of<native_func(int)>::type y; // No
object_func of;
call_func(of); // Yes
call_func(native_func); // Yes
}

参考了StackOverflow上的答案这是因为原生函数并不是一个 type,但是std::result_of<F(Args...)>中的 F 必须要是一个类型,合适的方法是使用decltype+declval,比如如下所示,而不是用std::result_of

1
decltype(native_func(std::declval<int>()))

此外,在 C++17 标准之后,可以使用std::invoke_result来替代std::result_of
此外,在这个回答中实现一个function_traits。这个类型类似 pattern matching,能够同时处理函数对象和函数指针的情况,并且能够推导出参数和返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
struct function_traits : public function_traits<decltype(&T::operator())> {};

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
{
enum { arity = sizeof...(Args) };
typedef ReturnType result_type;
template <size_t i>
struct arg
{
typedef typename std::tuple_element<i, std::tuple<Args...>>::type type;
};
};

在类模板中声明友元函数模板

在一个模板类farray中声明一个友元函数

1
2
3
4
5
6
7
8
9
template<typename T>
struct farray{
// ...
friend farray<bool> operator<(const farray<T> & x, const T & y);
}
template <typename T>
farray<bool> operator<(const farray<T> & x, const T & y){
// ...
}

可能会报错

1
error LNK2019: 无法解析的外部符号 "struct for90std::farray<bool> __cdecl for90std::operator<(struct for90std::farray<int> const &,int const &)" (??Mfor90std@@YA?AU?$farray@_N@0@AEBU?$farray@H@0@AEBH@Z)

原因是T是类模板farray的模板参数,随着类模板特化。所以应该给友元函数operator<独立的模板参数,改成这样就好了

1
template<typename U> friend farray<bool> operator<(const farray<U> & x, const U & y);

详细可以参考这篇答案

两阶段名字查找(Two-phase name look up)

两阶段名字查找模型是表示一系列用来决议C++模板声明中使用到的名字的规则,它最初的提出是用来在inclusion model(现在用的)和separation model这两种模板实现方式之间进行妥协的。
两阶段名字查找模型规定了模板函数中出现的所有名字的查找方式。我们首先定义non-dependent名字,一个non-dependent名字在查找时不依赖任何的模板参数,所以对于任意实例化后的模板它都是一样的。相反一个dependent名字依赖于模板参数,例如A<X>::B b中的bthis都是dependent的名字。此外对于像A::B这样的名字,编译器可能并不知道它是一个函数还是一个类型或者一个变量,所以当做类型使用时,我们需要显式指定一下typename来通告编译器。不过有的时候大家就滥用,有的没的都加上typename,会报错expected a qualified name after 'typename'。这是因为只有在嵌套从属名称前面才要加上typename,其他的例如typename C & cont的用法是错误的。

  1. 对于non-dependent符号,该符号在模板定义阶段即被查找决议(all non-dependent names are resolved (looked up)),此外在第一阶段还会做一些语法检查。
  2. 对于dependent符号,查找被推迟到模板被实例化(point of instantiation, POI)时。

在早期版本中,MSVC不支持two phase name lookup,由此带来的结果就是很多MSVC能编译的代码在GCC等编译器上并不能够编译。我们查看下面的代码

1
2
3
4
5
6
7
void func(void*) { std::puts("The call resolves to void*") ;}
template<typename T> void g(T x)
{
func(0);
}
void func(int) { std::puts("The call resolves to int"); }
int main() { g(3.14); }

根据两阶段查找,func是无依赖的,由此在模板定义时就会被决议。因此虽然g中调用的func(0)函数的最佳决议应该是void func(int);,但是由于此时g尚未看到void func(int);,所以只能决议到void func(void *),这也就是gcc的标准实现。但MSVC的某些版本很懒,根本不在模板定义阶段做事,所以在模板实例化阶段我们已经能够看到最佳适配的void func(int);了。

常用术语

参数包:parameter pack
类型限定符(const, volatile):qualification adjustment
返回类型后置:trailing return type
函数签名:signature
模板类型推导:template argument deduction
聚合体Aggregates和POD(Plain Old Data)=trival+standard layout:见此解释
非类型模板参数:non type template parameter
包扩展:pack expansion
花括号初始化器:brace-init-list
省略号(…):ellipsis
符号扩展、零扩展:sign/zero extension
可变参数模板:Variadic Templates pack
展开模式:pattern
决议:resolution