在 《通过返回值’重载’函数》 中提到这样一个需求:设计一个计算函数耗时的接口,针对有返回值的函数,这个接口返回耗时和函数的返回值;对于没有返回值的函数,这个接口只返回耗时。
期望的的调用方式如下:
1
2
| (cost, ret) = costTimeMs(funcA, a, b);
cost = costTimeMs(funcB, c, d, e);
|
改造costTimeMs
在这不讨论这个需求和接口的合理性,上述链接中通过std::enable_if
实现了这个需求。最近学习了另一种技巧 – tag dispatch
即标签派发,本文会通过标签派发实现这个需求。
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
| struct has_return_tag {};
struct no_return_tag {};
template <typename T>
struct return_tag {
using tag = has_return_tag;
};
template <>
struct return_tag<void> {
using tag = no_return_tag;
};
template<typename F, typename ...Args>
std::pair<double, typename std::invoke_result_t<F, Args...>>
costTimeMs(has_return_tag, F&& f, Args&&... args) {
const auto start = std::chrono::high_resolution_clock::now();
const auto ret = std::forward<F>(f)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return {std::chrono::duration<double>(end - start).count(), ret};
}
template<typename F, typename ...Args>
double
costTimeMs(no_return_tag, F&& f, Args&&...args) {
const auto start = std::chrono::high_resolution_clock::now();
std::forward<F>(f)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration<double>(end - start).count();
}
template<typename F, typename ...Args>
auto costTimeMs(F&& f, Args&&... args) {
using tag = typename return_tag<typename std::invoke_result_t<F, Args...>>::tag;
return costTimeMs(tag{}, std::forward<F>(f), std::forward<Args>(args)...);
}
// ...
auto foo1 = []() -> int32_t {return 1;};
auto foo2 = []() -> void {};
static_assert(sizeof(costTimeMs(foo1)) > 8);
static_assert(sizeof(costTimeMs(foo2)) == 8);
|
标签类型需要放到可变参的前面,否则可能被当作可变参的一部分。
并不像concept
适用与C++20,这个技巧也适用于C++11,替换上述实现中与C++17相关的部分,这样实现:
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
| template<typename F, typename ...Args>
std::pair<double, typename std::result_of<F(Args...)>::type>
costTimeMs(has_return_tag, F&& f, Args&&... args) {
const auto start = std::chrono::high_resolution_clock::now();
const auto ret = std::forward<F>(f)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return {std::chrono::duration<double>(end - start).count(), ret};
}
template<typename F, typename ...Args>
double
costTimeMs(no_return_tag, F&& f, Args&&...args) {
const auto start = std::chrono::high_resolution_clock::now();
std::forward<F>(f)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration<double>(end - start).count();
}
template<typename F, typename ...Args,
typename result_t = typename std::result_of<F(Args...)>::type,
typename tag_t = typename return_tag<result_t>::tag>
auto costTimeMs(F&& f, Args&&... args)
-> typename std::conditional<std::is_same<result_t, void>::value,
double,
std::pair<double, result_t>>::type {
return costTimeMs(tag_t{}, std::forward<F>(f), std::forward<Args>(args)...);
}
|
仅有invoke_result_t
和auto
返回值推导的变化。
一般实现
总结上述实现,标签派发技术看起来有几个关键点:
- 定义不同的标签类型,一般是空结构体,比如
has_return_tag
和no_return_tag
- 定义一个可以取得不同标签的接口,比如
return_tag
- 根据不同的标签类型,实现不同的函数重载(注意是重载,而不是
SFINAE
) - 实现用户接口,屏蔽标签类型的细节
最后一点也并非必须,因为关注到了std::adopt_lock_t等类型,实际上就是一个标签类型,用于给用户选择。
暴露标签有什么好处呢?比如std::unique_lock
的实现:
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
| template<typename _Mutex>
class unique_lock
{
public:
typedef _Mutex mutex_type;
unique_lock() noexcept
: _M_device(0), _M_owns(false)
{ }
explicit unique_lock(mutex_type& __m)
: _M_device(std::__addressof(__m)), _M_owns(false)
{
lock();
_M_owns = true;
}
unique_lock(mutex_type& __m, defer_lock_t) noexcept
: _M_device(std::__addressof(__m)), _M_owns(false)
{ }
unique_lock(mutex_type& __m, try_to_lock_t)
: _M_device(std::__addressof(__m)), _M_owns(_M_device->try_lock())
{ }
unique_lock(mutex_type& __m, adopt_lock_t) noexcept
: _M_device(std::__addressof(__m)), _M_owns(true)
{
// XXX calling thread owns mutex
}
//...
};
|
可以根据不同的lock_tag
进行不同的构造,如果让我实现,考虑有几种方式:
- 模板
if constexpr
条件判断- 使用
constexpr
可以在编译期确定,个人认为还不错,但是在unique_lock
这个具体的例子上也不好,这样就无法使用构造初始化,只能赋值初始化 - 另一个原因是注意到
try_to_lock_t
并没有noexcept
保证,我们得保留这个特性
tag dispatch
cppreference也给出过建议,优先考虑if constexpr
和tag dispatch
,最后是SFINAE。
Where applicable, tag dispatch, if constexpr(since C++17), and concepts (since C++20) are usually preferred over use of SFINAE.
更简单的方式
另外,在接受这个接口需求设计的前提下,抛开tag dispatch
,使用C++17则可以有更简单的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| template<typename F, typename ...Args>
auto costTimeMs(F&& f, Args&&... args) {
using result_t = typename std::invoke_result_t<F, Args...>;
if constexpr (std::is_same_v<result_t, void>) {
const auto start = std::chrono::high_resolution_clock::now();
std::forward<F>(f)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration<double>(end - start).count();
} else {
const auto start = std::chrono::high_resolution_clock::now();
const auto ret = std::forward<F>(f)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return std::pair{std::chrono::duration<double>(end - start).count(), ret};
}
}
|
参考链接
参考链接如下,其中我是在《CppCoreGuidelines-zh-CN》接触到这个概念的。这些技术(技巧)就像《tagged-pointer-让指针包含更多信息》一样,让我眼前一亮,原来还可以这么玩;不过始终是要用起来了才是好的。