通过返回值'重载'函数

以下的代码片段涉及到了不少的模板函数, 可以自行去官网查询.

前言

从实际问题出发, 期望开发一个函数, 可以计算另外一个函数的耗时; 比如测试下面函数的耗时

1
2
int funcA(int &a, float &b);
void funcB(bool &c, char &d, double &e);

期望可以这样调用:

1
2
(cost, ret) = costTime(funcA, a, b);
cost = costTime(funcB, c, d, e);

对于有返回值的函数, 不仅需要计算函数的耗时, 还需要能过获取函数的返回值; 对没有返回值的函数, 则只需要计算函数的耗时;

有多种设计思路, 一是上述的代码, 通过返回值获取时间和被测函数的返回值;

二可以通过函数参数获取时间和被测函数返回值, 但是第二种方法总有一点不适合, 因为其结构不是很美观.(一般第一个参数是func, 最后的参数是args, 那么时间或被测函数的返回值就在参数中间, 太不好看了!!!)

以下, 仅针对第一种方法, 看看怎么设计这个函数;

函数设计准备

编写只有一个返回值的函数来测试函数耗时比较简单, 通过可变长模板就很容易实现了, 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template <typename Func, typename... Args>
static double
costTimeMs(Func&& func, Args&&... args)
{
    auto get_time_us = [] () -> double {
        struct timeval time;
        gettimeofday(&time, NULL);
        double ms = time.tv_sec * 1000.0 + time.tv_usec / 1000.0;
        return ms;
    };

    double start = get_time_us();
    func(args...);
    double end = get_time_us();

    double cost = end - start;

    return cost;
}

但是, 还有一个需求, 就是期望也能输出函数的返回值, 并且期望函数的调用接口不要变, 怎么设计呢? 其实有点像是函数重载, 但是这里是针对函数返回值的重载.

C++为我们提供了enable_if这个模板函数.

enable_if

下面摘取官方文档中的源码

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; };

当使用enable_if的时候, 没有任何返回; 当是用enable_if的时候, 可以返回一个T类型;

刚开始看这个函数的时候不太好理解, 我们来实验一下吧. 以下改编自官方demo, 在验证的时候, 一定要结合编译报错信息帮助理解.

在线编译器

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <type_traits>
#include <iostream>
#include <string>

using namespace std;

template<class T, class Enable = void>
class A {
public:
    A(){
        cout << 111 << endl;
    }
}; // primary template

template<class T>
class A<T, typename std::enable_if<std::is_void<T>::value>::type> {
public:
    A(){
        cout << 333 << endl;
    }

    template<typename TT>
    static
    typename std::enable_if<std::is_integral<TT>::value, char>::type
    func(TT a)
    {
        cout << "444" << endl;
        return 'a';
    }
    template<typename TT>
    static
    typename std::enable_if<std::is_floating_point<TT>::value, int>::type
    func(TT a)
    {
        cout << "555" << endl;
        func(0);
        cout << "666" << endl;
        return 1;
    }
    template<typename Func, typename... Args>
    static
    typename std::enable_if<std::is_void<typename std::result_of<Func(Args...)>::type>::value, float>::type
    func(Func&& f, Args&&... args)
    {
        cout << "777" << endl;
        f(args...);
        cout << "888" << endl;
        return 1.1;
    }
};

int main()
{
    A<int>{}; // OK, 用第一个A
    A<double>{}; // OK, 用第一个A
    A<void> a; // OK, 用第二个A
    cout << A<void>::func(0.0) << endl;
    cout << A<void>::func(0) << endl;

    auto f = [] (int a, int b) -> void {
        cout << a << b << endl;
    };

    cout << A<void>::func(f, 999, 999) << endl;
}

输出是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
111
111
333
555
444
666
1
444
a
777
999999
888
1.1

说明, 给A的模板传入int和double类型的时候, 调用的都是第一个A的实现, 因为这时候没有其他匹配的模板;

给A的模板传入void类型的时候, 调用的是第二个A.

1
2
template<class T>
class A<T, typename std::enable_if<std::is_void<T>::value>::type> {}

使用A::func(0.0)函数, 调用的这是第二个A里面的通过float匹配的func函数…其余可以自行配对看看调用的是哪个类和哪个函数.

接下来我们写一点bug, 让编译器报错, 看看它怎么说; 这样改写第二个A, 把与int匹配的func去掉, 编译看看

 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
template<class T>
class A<T, typename std::enable_if<std::is_void<T>::value>::type> {
public:
    A(){
        cout << 333 << endl;
    }

    template<typename TT>
    static
    typename std::enable_if<std::is_floating_point<TT>::value, int>::type
    func(TT a)
    {
        cout << "555" << endl;
        func(0);
        cout << "666" << endl;
        return 1;
    }
    template<typename Func, typename... Args>
    static
    typename std::enable_if<std::is_void<typename std::result_of<Func(Args...)>::type>::value, float>::type
    func(Func&& f, Args&&... args)
    {
        cout << "777" << endl;
        f(args...);
        cout << "888" << endl;
        return 1.1;
    }
};

编译报错, 它说了什么呢? 它找遍了A里面的所有的func函数, 期望找到一个匹配的, 结果都没找到, 相当于是func没有定义, 最后抛出"error: no matching function for call to ‘func’“的错误.

 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
<source>:50:13: error: no matching function for call to 'func'
    cout << A<void>::func(0) << endl;
            ^~~~~~~~~~~~~
<source>:25:5: note: candidate template ignored: requirement 'std::is_floating_point<int>::value' was not satisfied [with TT = int]
    func(TT a)
    ^
<source>:35:5: note: candidate template ignored: substitution failure [with Func = int, Args = <>]: no type named 'type' in 'std::result_of<int ()>'
    func(Func&& f, Args&&... args)
    ^
<source>:28:9: error: use of undeclared identifier 'func'
        func(0);
        ^
<source>:49:22: note: in instantiation of function template specialization 'A<void, void>::func<double>' requested here
    cout << A<void>::func(0.0) << endl;
                     ^
<source>:25:5: note: must qualify identifier to find this declaration in dependent base class
    func(TT a)
    ^
<source>:35:5: note: must qualify identifier to find this declaration in dependent base class
    func(Func&& f, Args&&... args)
    ^
<source>:28:9: error: no matching function for call to 'func'
        func(0);
        ^~~~
<source>:25:5: note: candidate template ignored: requirement 'std::is_floating_point<int>::value' was not satisfied [with TT = int]
    func(TT a)
    ^
<source>:35:5: note: candidate template ignored: substitution failure [with Func = int, Args = <>]: no type named 'type' in 'std::result_of<int ()>'
    func(Func&& f, Args&&... args)
    ^
3 errors generated.

所以, 对enable_if, 不太准确的说就是, enable_if是一个函数开关, 它通过第一个模板的true/false来使能函数, 如果是false, 则函数直接被"关闭”(不使能), 如果是true, 则可以把第二个参数(类型)作为函数的返回类型(当然也可以不使用, 仅做开关也可).

耗时计算函数

以上, 我们使用enable_if可以来设计我们的计算耗时的函数了. 直接上代码

 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
//当被测试函数的返回值  是void 的时候, 调用这个函数, 返回值的double类型的毫秒时间
template <typename Func, typename... Args>
static
typename std::enable_if<std::is_void<typename std::result_of<Func(Args...)>::type>::value, double>::type
costTimeMs(Func&& func, Args&&... args)
{
    auto get_time_us = [] () -> double {
        struct timeval time;
        gettimeofday(&time, NULL);
        double ms = time.tv_sec * 1000.0 + time.tv_usec / 1000.0;
        return ms;
    };

    double start = get_time_us();
    func(args...);
    double end = get_time_us();

    double cost = end - start;

    return cost;
}

//当被测试函数的返回值  非void  的时候, 调用这个函数, 返回值的tuple, 第一个值是double类型的毫秒时间, 第二个值是函数的返回值
template <typename Func, typename... Args>
static
typename std::enable_if<!(std::is_void<typename std::result_of<Func(Args...)>::type>::value),
std::tuple<double, typename std::result_of<Func(Args...)>::type>>::type
costTimeMs(Func&& func, Args&&... args)
{
    using RT = typename std::result_of<Func(Args...)>::type;
    std::tuple<double, RT> ret;
    auto nfunc = [&] () -> void {
        std::get<1>(ret) = func(args...);
    };
    std::get<0>(ret) = costTimeMs(nfunc);

    return ret;
}

使用typename std::result_of<Func(Args…)>::type获取函数返回值类型, 如果是void, 则调用第一个函数, 如果不是void则调用第二个函数, 第二个函数会稍微改装以下func, 使其满足返回值是void的样式.

通过返回值"重载"函数, 到此就好了.

原因

为什么可以这么工作呢? 其实还是比较好理解的.

模板函数编译与否, 要不要参与编译是在编译期就确定的(废话).

比如使用enable_if这个模板函数, 在编译的时候会匹配这个模板, 如果匹配上了, 那么就会返回一个类型, 正常编译; 如果没有匹配上, enable_if就什么也不返回, 函数编译不过; 比如尝试匹配后的两个函数:

1
2
void funcA();       //1
funcA();            //2

显然, 第二个是编译不过的, 但是编译器不会报错, 因为有正确匹配版本的funcA. (这里的意思是, 使用enable_if等, 需要考虑的所有情况, 不然可能会有某个调用找不到匹配的模板, 就会报错.)