tricks-of-CPP

在C++中,有一些不太常见的小技巧,甚至有些能够被称之为黑魔法

判断类有没有某个方法,有这个方法才去调用

当你的代码需要同时服务某个库的新旧版本,希望调用新增接口,又不希望这个库的旧版本因为没有这个接口报错

对C++的基础库开发者来说,这是很常见的需求,因为你的库依赖的底层库,不一定可以强行指定版本,那么此时这个兼容性神器就可以出场了

这是一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <type_traits>

template <typename...>
using void_t_cpp11 = void;

template <typename T, typename = void>
struct isNewVersion : std::false_type {};

template <typename T>
struct isNewVersion<T,
void_t_cpp11<decltype(std::declval<T>().newVersionInterface())>>
: std::true_type {};

template <typename T,
typename enable_if<isNewVersion<T>::value>::type* = nullptr>
void newVersionInterface(T &t) {
t.newVersionInterface();
// static_assert(sizeof(t) == 0, "newVersionInterface found");
}
template <typename T,
typename enable_if<!isNewVersion<T>::value>::type* = nullptr>
void newVersionInterface(T &t) {
// static_assert(sizeof(t) == 0, "newVersionInterface not found");
}

以上代码可以在C++11下正常工作

isNewVersion这个模板类有两个模板参数,其中typename T是被判断的类型,第二个模板参数要结合偏特化来看

isNewVersion这个模板类的偏特化的第二个参数是void_t_cpp11(如果在C++17下,可以直接使用std::void_t),void_t_cpp11在传入任何类型下都将返回void,因此isNewVersion的偏特化就是基于void类型的

void_t_cpp11内是一个decltype表达式,用declval创建了T类型,调用newVersionInterface,如果T类型存在newVersionInterface,那么decltype表达式成立,这个偏特化就可以成立

非偏特化版本的isNewVersion的第二个模板参数是默认模板参数typename = void,因此使用一个参数的isNewVersion<T>相当于isNewVersion<T, void>,会默认指向void偏特化类型,根据SFINAE,如果偏特化成立,就会匹配到偏特化的版本,继承了true_type,否则降级到非偏特化版本,继承了false_type

这两个继承的type都提供了一个false或者true的value字段,因此根据isNewVersion<T>::value就可以得到T类型是否实现了newVersionInterface方法

随后再次使用SFINAE实现一个模板函数newVersionInterface,当isNewVersion<T>::value 为true时,就可以对t调用newVersionInterface方法,否则什么都不做

将static_assert的注释去掉,写一个测试程序验证一下

1
2
3
4
5
6
7
8
9
10
11
struct A{
void newVersionInterface();
};

struct B {
};

A a;
newVersionInterface(a);
B b;
newVersionInterface(b);

编译得到

1
2
3
4
5
6
7
8
9
10
/root/cpp_test/temp.cpp: In instantiation of ‘void newVersionInterface(T&) [with T = A; typename std::enable_if<isNewVersion<T>::value>::type* <anonymous> = 0]’:
/root/cpp_test/temp.cpp:37:26: required from here
/root/cpp_test/temp.cpp:25:29: error: static assertion failed: newVersionInterface found
25 | static_assert(sizeof(t) == 0, "newVersionInterface found");
| ~~~~~~~~~~^~~~
/root/cpp_test/temp.cpp: In instantiation of ‘void newVersionInterface(T&) [with T = B; typename std::enable_if<(! isNewVersion<T>::value)>::type* <anonymous> = 0]’:
/root/cpp_test/temp.cpp:39:26: required from here
/root/cpp_test/temp.cpp:30:29: error: static assertion failed: newVersionInterface not found
30 | static_assert(sizeof(t) == 0, "newVersionInterface not found");
| ~~~~~~~~~~^~~~

可以看到A类assert到存在newVersionInterface方法,B类assert到不存在,符合预期

static_assert

这里的 static_assert 利用了 C++ 模板的二阶段查找(two-phase name lookup)特性,在 C++ 的模板编译过程中,会分成模板参数检查阶段和模板实例化阶段

在模板参数检查阶段,编译器不会具体检查模板函数体中的依赖模板的表达式,只有模板函数被具体的类型实例化时,才会检查函数体中依赖模板的表达式

static_assert(sizeof(t) == 0, "xxx")中的sizeof(t)一定是不为0的,所以在某个类型具体实例化的时候,才会因为assert出希望打印的字符串

实际使用场景

需求:

函数getMember有两个参数,第一个值参数和第二个该值的成员指针参数

伪代码如下:

1
2
template <typename T, typename MemberPtr>
auto& getMember(T &t, MemberPtr ptr);

值参数有可能传入原生指针或者智能指针,要能正确区分最后获取到该值的成员

  • 第一步是判断类型是不是指针,如果是的话获取解引用类型,否则获取当前类型

​ 这就需要通过非原生指针类型存在operator->()方法,从而实现isPtr模板类,随后再使用偏特化模板类实现getPtrType模板类

  • 第二步就是通过getPtrType模板类进行函数重载,实现getMember函数
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <type_traits>

template <typename ...>
using void_t_cpp11 = void;

template <typename T, typename = void>
struct isCustomPtr : public std::false_type {};

template <typename T>
struct isCustomPtr<T,
void_t_cpp11<decltype(std::declval<T>().operator->())>> :
public std::true_type {
};

测试一下isPtr

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <memory>

using namespace std;

int main() {
cout << isCustomPtr<int>::value << endl;
cout << isCustomPtr<shared_ptr<int>>::value << endl;
}

输出

1
2
0
1

符合预期,接下来是实现getPtrType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T, typename = void>
struct getPtrType {
using type = T;
};

template <typename T>
struct getPtrType<T,
typename std::enable_if<std::is_pointer<T>::value>::type> {
using type = typename std::remove_reference<decltype(*std::declval<T>())>::type;
};

template <typename T>
struct getPtrType<T,
typename std::enable_if<isCustomPtr<T>::value>::type> {
using type = typename std::remove_reference<decltype(*std::declval<T>().operator->())>::type;
};

这里的原理和isCustomPtr是类似的,非偏特化版本的第二个默认模板参数为void

随后如果enable_if能够满足,那么就会变成void类型,所以getPtrType<T>的默认void就会指向这个偏特化类型

编译器打印一下类型验证一下

编译器打印模板参数类型print_type

1
2
3
4
5
6
7
8
9
10
11
template <typename>
struct print_type;

int main() {
{
print_type<getPtrType<int*>::type> dummy[1];
}
{
print_type<getPtrType<shared_ptr<int>>::type> dummy[1];
}
}

这也是一个很常用的小技巧,通过构造一个不完整类型的数组(单个元素不行),可以让编译器报错打印出类型

1
2
3
4
5
6
7
8
9
/root/cpp_test/temp.cpp: In function ‘int main()’:
/root/cpp_test/temp.cpp:70:44: error: elements of array ‘print_type<int> dummy [1]’ have incomplete type
70 | print_type<getPtrType<int*>::type> dummy[1];
| ^~~~~
/root/cpp_test/temp.cpp:70:44: error: storage size of ‘dummy’ isn’t known
/root/cpp_test/temp.cpp:73:55: error: elements of array ‘print_type<int> dummy [1]’ have incomplete type
73 | print_type<getPtrType<shared_ptr<int>>::type> dummy[1];
| ^~~~~
/root/cpp_test/temp.cpp:73:55: error: storage size of ‘dummy’ isn’t known

可以看到两次print_type数组,都报错出print_type<int> dummy [1] have incomplete type,print_type内的int类型就是getPtrType<T>::type预期获取的类型

getMember实现

getMember的实现相对简单,唯一要注意的是指针类型可能传入shared_ptr<const T>类型,从而匹配非const函数,因此返回值需要返回auto &

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct A {
int a = 0;
};

template <typename K, typename T>
K& getMember(T &t, K T::* ptr) {
return t.*ptr;
}

template <typename K, typename T>
const K& getMember(const T &t, K T::* ptr) {
return t.*ptr;
}

template <typename K, typename T>
auto& getMember(T &t, K getPtrType<T>::type::* ptr) {
return (*t).*ptr;
}

template <typename K, typename T>
const K& getMember(const T &t, K getPtrType<T>::type::* ptr) {
return (*t).*ptr;
}

测试代码:

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
using namespace std;

int main() {
A a;
cout << getMember(a, &A::a) << endl;
{
const A a;
cout << getMember(a, &A::a) << endl;
}
{
auto aPtr = make_shared<A>();
cout << getMember(aPtr, &A::a) << endl;
}
{
auto aPtr = make_shared<const A>();
cout << getMember(aPtr, &A::a) << endl;
}
{
A* aPtr = &a;
cout << getMember(aPtr, &A::a) << endl;
}
{
const A* aPtr = &a;
cout << getMember(aPtr, &A::a) << endl;
}
}

编译正确通过

更通用的实现

通用写法用于支持多类型的情况,例如需要判断某个类实现的newVersionInterface函数是否支持某个类型的参数

实现的逻辑比较简单,不做解释了

1
2
3
4
5
6
7
8
9
10
11
12
template <typename ...>
using void_t_cpp11 = void;

template <template <typename ...> class Expression, typename DefaultT, typename ... Args>
struct is_detected_impl : std::false_type {};

template <template <typename...> class Expression, typename... Args>
struct is_detected_impl<Expression, void_t_cpp11<Expression<Args...>>, Args...>
: std::true_type {};

template <template <typename ...> class Expression, typename ... Args>
constexpr bool is_detected = is_detected_impl<Expression, void, Args...>::value;

这里使用is_detected来默认给is_detected_impl传入第二个为void类型,从而匹配到偏特化中的void_t_cpp11类型

在实现了通用模板以后,就可以实现具体的newVersionInterface了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T, typename U>
using isNewVersionExpression = decltype(std::declval<T>().newVersionInterface(std::declval<U>()));

template <typename T,
typename std::enable_if<is_detected<isNewVersionExpression, T, int>>::type* = nullptr>
void newVersionInterface(T &t, int a) {
t.newVersionInterface(a);
static_assert(sizeof(t) == 0, "newVersionInterface found");
}
template <typename T,
typename std::enable_if<!is_detected<isNewVersionExpression, T, int>>::type* = nullptr>
void newVersionInterface(T &t, int) {
static_assert(sizeof(t) == 0, "newVersionInterface not found");
}

最后是使用newVersionInterface的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct A{
void newVersionInterface(int);
};

struct B {
void newVersionInterface(void);
};

struct C {
};

int main() {
A a;
newVersionInterface(a, 0);
B b;
newVersionInterface(b, 0);
C c;
newVersionInterface(c, 0);
}

编译打印报错来验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/root/cpp_test/temp32.cpp: In instantiation of ‘void newVersionInterface(T&, int) [with T = A; typename std::enable_if<is_detected<isNewVersionExpression, T, int> >::type* <anonymous> = 0]’:
/root/cpp_test/temp32.cpp:45:29: required from here
/root/cpp_test/temp32.cpp:23:29: error: static assertion failed: newVersionInterface found
23 | static_assert(sizeof(t) == 0, "newVersionInterface found");
| ~~~~~~~~~~^~~~
/root/cpp_test/temp32.cpp: In instantiation of ‘void newVersionInterface(T&, int) [with T = B; typename std::enable_if<(! is_detected<isNewVersionExpression, T, int>)>::type* <anonymous> = 0]’:
/root/cpp_test/temp32.cpp:47:29: required from here
/root/cpp_test/temp32.cpp:28:29: error: static assertion failed: newVersionInterface not found
28 | static_assert(sizeof(t) == 0, "newVersionInterface not found");
| ~~~~~~~~~~^~~~
/root/cpp_test/temp32.cpp: In instantiation of ‘void newVersionInterface(T&, int) [with T = C; typename std::enable_if<(! is_detected<isNewVersionExpression, T, int>)>::type* <anonymous> = 0]’:
/root/cpp_test/temp32.cpp:49:29: required from here
/root/cpp_test/temp32.cpp:28:29: error: static assertion failed: newVersionInterface not found

编译的报错信息显示

A实现的newVersionInterface方法,提供了int类型参数,找到了预期方法

B实现的newVersionInterface方法提供了void类型,所以没找到

C没有实现newVersionInterface方法,所以没找到

类内模板全特化

1
2
3
4
5
6
7
struct Test {
template <bool>
struct SubTest {};

template <>
struct SubTest<true> {};
};

在C++17以前,模板的全特化不能在类内进行,标准可见C++11的n3376-14.7.3/2

An explicit specialization shall be declared in a namespace enclosing the specialized template

在C++17以后可以,标准可见C++17的17.7.3/2

An explicit specialization may be declared in any scope in which the corresponding primary template may be defined

C++17的验证可以在clangd 18.1.0上成功编译

但是在gcc 14.1会编译失败,应该是gcc的bug,报错:

1
2
3
/root/cpp_test/temp33.cpp:5:15: error: explicit specialization in non-namespace scope ‘struct Test’
5 | template <>
| ^

很多时候全特化的模板类不希望暴露到类外,所以对于主要平台是gcc的我来说,必须绕过这个问题

思路就是使用偏特化代替全特化

1
2
3
4
5
6
7
struct Test {
template <bool, typename = void>
struct SubTest {};

template <typename dummy>
struct SubTest<true, dummy> {};
};

lambda装饰器:给需要运行的函数添加固定逻辑

这是一个很常见的需求,例如在服务端的rpc接口内打印接口名,耗时,最重要的是try,catch异常

在C++11以前的凑活方案

往往只能使用RAII的方式在rpc接口的声明变量来打印耗时和返回值,然后使用一个__TRY____CATCH__宏来简化复杂的try, catch

伪代码大概如下:

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
struct ScopeLog {
ScopeLog(const string &fileName) : fileName_(fileName), startTimeMs_(getNowMs()){}
~ScopeLog() {
instantLog(LOG); //LOG宏会返回一个继承os的对象,这个对象析构的时候就会打印日志
}
void instantLog( std::ostream &os ) {
//统计耗时等,最后append endl
...
os << "cost: " << getNowMs() - startTimeMs_;
os << endl;
}
string fileName_;
int64_t startTimeMs_;
};

#define __TRY__ try{
#define __CATCH__ }catch(exception& ex) \
{ \
log << "exception: " << ex.what() << endl; \
} \
catch(...) \
{ \
log << "unknown exception" << endl; \
}

struct MyServer {
int myInterface(const Req &req, Rsp &rsp) override { //重写一个IDL生成的接口函数
int ret = 0;
ScopeLog log;
log << __FUNCTION__ << "|" << req << "|" << rsp << "|" << ret;
__TRY__
//业务逻辑
...
__CATCH__
return ret;
}
};

可以看到,myInterface需要编写大量重复性很高也很容易出错的代码

从C++11开始,有缺陷的宏解决方案

C++11开始的lambda结合宏可以更好的解决这个问题

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
#include <exception>
#include <iostream>

template <typename FuncType>
const FuncType& make_function_wrapper(const FuncType &func) {
return func;
}

#define TryCatchFunc(...) \
do {\
auto &f = make_function_wrapper(__VA_ARGS__);\
try {\
f();\
}catch (std::exception &e) {\
std::cout << e.what() << std::endl;\
}catch (...) {\
std::cout << "unknown exception" << std::endl;\
}\
/*成功调用了,统计耗时等打印*/\
}while(0);

int main() {
int ret = 0;
TryCatchFunc([=, &ret]() {
ret = 1;
});
std::cout << ret << std::endl; //输出1,运行成功
}

这里必须使用宏可变参数传递给make_function_wrapper这个模板函数来捕获传入的lambda参数,这是因为一旦lambda出现逗号,例如这里的[=, &ret]

宏会将其识别成多个参数,从而出现报错

例如改成这样

1
2
3
4
5
6
7
8
9
10
#define WrongTryCatchFunc(f) \
do {\
try {\
f();\
}catch (std::exception &e) {\
std::cout << e.what() << std::endl;\
}catch (...) {\
std::cout << "unknown exception" << std::endl;\
}\
}while(0);

报错

1
2
/root/cpp_test/temp.cpp:41:6: error: macro "WrongTryCatchFunc" passed 2 arguments, but takes just 1
41 | });

myInterface的伪代码使用TryCatchFunc来改写一下

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
#define ServerTryCatchFunc(...) \
do {\
auto now = getNowMs();\
auto &f = make_function_wrapper(__VA_ARGS__);\
try {\
int ret = f();\
}catch (std::exception &e) {\
LOG << __FUNCTION__ << "|" << \
req << "|" << e.what() << "|cost: " << getNowMs() - now << endl;\
}catch (...) {\
LOG << __FUNCTION__ << "|" << req << \
"|unknown exception" << "|cost: " << getNowMs() - now << endl;\
}\
LOG << __FUNCTION__ << "|" << req << \
"|" << rsp << "|" << ret << "|cost: " << getNowMs() - now << endl;
return ret;
}while(0);

struct MyServer {
int myInterface(const Req &req, Rsp &rsp) override { //重写一个IDL生成的接口函数
ServerTryCatchFunc([&]() {
//业务逻辑
int ret = 0;
...
return ret;
});
}
};

可以看到,MyServer的实现简单了很多,大致能满足需求了

但是TryCatchFunc本身作为宏,在这个需求下适用,但是作为通用需求,还是很容易踩到预期外的坑

更重要的是,这也不能称之为lambda装饰器,只能调用无参数的lambda而已

正道还是需要使用模板来实现的

从C++11开始的模板解决方案

C++17的apply

C++17开始引入了std::apply函数,可以很方便的解决这个问题

1
2
template< class F, class Tuple >
constexpr decltype(auto) apply( F&& f, Tuple&& t );

传入一个函数f和std::tuple的t,会将t拆解成参数交给函数f来调用

所以设计一个模板参数,将变长参数列表打包成tuple后交给apply使用即可

另外有返回值和没有返回值的实现是不一样的

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
#include <exception>
#include <iostream>
#include <tuple>

//获取retType,如果是void,那么就不需要返回值
//获取retType时,参数的类型注意是std::tuple<ArgsType&&>,在template里面不能写decltype(std::forward_as_tuple(args...))
template <typename FuncType, typename... ArgsType,
typename retType = decltype(std::apply(std::declval<FuncType>(), std::declval<std::tuple<ArgsType&&...>>())),
typename std::enable_if<std::is_void<retType>::value>::type * = nullptr>
void tryCatchFunc(const FuncType& func, ArgsType&&... args) {
auto args_tuple = std::forward_as_tuple(args...); //make_tuple只是会复制为左值,这里必须用forward_as_tuple来完美转发
try {
std::apply(func, args_tuple);
}catch (std::exception &e) {
std::cout << e.what() << std::endl;
}catch (...) {
std::cout << "unknown exception" << std::endl;
}
}

template <typename FuncType, typename... ArgsType,
typename retType = decltype(std::apply(std::declval<FuncType>(), std::declval<std::tuple<ArgsType&&...>>())),
typename std::enable_if<!std::is_void<retType>::value>::type * = nullptr>
retType tryCatchFunc(const FuncType& func, ArgsType&&... args) {
auto args_tuple = std::forward_as_tuple(args...);
retType ret;
try {
ret = std::apply(func, args_tuple);
}catch (std::exception &e) {
std::cout << e.what() << std::endl;
}catch (...) {
std::cout << "unknown exception" << std::endl;
}
return ret;
}

int main() {
int v = 0;
int returnV = tryCatchFunc([=, &v](int value) {
v = value;
return v;
}, 2);
std::cout << v << std::endl; //输出2
std::cout << returnV << std::endl; //输出2
}

这里还有一个小瑕疵,就是__FUNCTION__只会获取到lambda的operator(),所以需要一个小技巧来获取当前的函数名

getFuncName模板函数获取当前函数名

使用cxxabi可以获取到类型名称,lambda的类型名称里面包含了定义这个函数的名称

所以首先实现getType模板函数获取lambda的类型名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cxxabi.h>
#include <memory>
#include <string>

inline std::string demangle(const char* mangled) {
int status;
std::unique_ptr<char[], void (*)(void*)> result(
abi::__cxa_demangle(mangled, 0, 0, &status), std::free);
return result.get() ? std::string(result.get()) : "error occurred";
}
template <typename T>
const std::string& getType() {
static std::string type = demangle(typeid(T).name()); //使用函数内静态变量来保证每个getType模板类的RTTI调用仅运行一次
return type;
}

对于刚才的例子,getType<FuncType>将返回main::{lambda(int)#2}

其中::是作用域,最后一个总是{lambda},倒数第二个就是真正的函数名

所以getFuncName模板函数做一个split操作,就可以获取到函数名了,如下

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
#include <vector>

std::vector<std::string> split(const std::string& s, const std::string& delimiter) {
std::vector<std::string> tokens;
std::string token;
size_t prev = 0, pos = 0;
do
{
pos = s.find(delimiter, prev);
if (pos == std::string::npos) pos = s.length();
token = s.substr(prev, pos - prev);
if (!token.empty()) tokens.push_back(token);
prev = pos + delimiter.length();
}
while (pos < s.length() && prev < s.length());
return tokens;
}

template <typename T>
const std::string& getFuncName() {
static std::string func = split(getType<T>(), "::").size() >= 2 ?
split(getType<T>(), "::")[split(getType<T>(), "::").size() - 2] :
"";
return func;
}

修改下tryCatchFunc,使用g++ -std=c++17 -o out.exe -g -Wall -lunwind temp.cpp重新编译

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 <typename FuncType, typename... ArgsType,
typename retType = decltype(std::apply(std::declval<FuncType>(), std::declval<std::tuple<ArgsType&&...>>())),
typename std::enable_if<std::is_void<retType>::value>::type * = nullptr>
void tryCatchFunc(const FuncType& func, ArgsType&&... args) {
auto args_tuple = std::make_tuple(args...);
try {
std::apply(func, args_tuple);
}catch (std::exception &e) {
std::cout << e.what() << std::endl;
}catch (...) {
std::cout << "unknown exception" << std::endl;
}
std::cout << getFuncName<FuncType>() << std::endl; //新增一行
}

template <typename FuncType, typename... ArgsType,
typename retType = decltype(std::apply(std::declval<FuncType>(), std::declval<std::tuple<ArgsType&&...>>())),
typename std::enable_if<!std::is_void<retType>::value>::type * = nullptr>
retType tryCatchFunc(const FuncType& func, ArgsType&&... args) {
auto args_tuple = std::make_tuple(args...);
retType ret;
try {
ret = std::apply(func, args_tuple);
}catch (std::exception &e) {
std::cout << e.what() << std::endl;
}catch (...) {
std::cout << "unknown exception" << std::endl;
}
std::cout << getFuncName<FuncType>() << std::endl; //新增一行,刚才的例子会输出main
return ret;
}

C++11自己实现apply

祖传代码怎么使用std::apply呢?那就只能自己实现辣

cpprefrerence的std::apply上可以得到提示

1
2
3
4
5
6
template<class F,class Tuple, std::size_t... I>
constexpr decltype(auto)
apply-impl(F&& f, Tuple&& t, std::index_sequence<I...>) // 仅用于阐述
{
return INVOKE(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...);
}

apply的实现是获取当前的参数个数,生成序列号,例如3个参数就是0, 1, 2

然后通过可变参数列表展开,std::get<0>(tuple),std::get<1>(tuple),std::get<2>(tuple),分别获取到参数再传递给func即可

在C++11没有std::index_sequence(C++14开始有),因此也需要自己实现一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <int ...data>
struct my_index_sequence {
template <int N>
using push_back = my_index_sequence<data..., N>; //展开可变参数列表, push_back需要新增的数字
};

template <int L, int R, typename = void>
struct my_make_index_sequence {
using ret =
typename my_make_index_sequence<L, R - 1>::ret
::template push_back<R>; //递归的设置ret为my_make_index_sequence<L, R - 1>::ret,再push-back当前的元素R
};

template <int L, int R>
struct my_make_index_sequence<L, R, typename std::enable_if<L == R>::type> {
using ret = my_index_sequence<L>; //模板偏特化来设置终止条件,当L == R时,ret为L一个数字
};

接下来就是实现my_apply了,由于C++11没有auto返回值(C++14开始有),所以return值看起来复杂了一点,"复读"了一遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename Func, typename Tuple, int ...S>
auto my_apply_imp(Func && func, Tuple && t, my_index_sequence<S...>) ->
decltype(std::forward<Func>(func)(std::get<S>(std::forward<Tuple>(t))...)) {
return std::forward<Func>(func)(std::get<S>(std::forward<Tuple>(t))...);
}

template <typename Func, typename Tuple,
int size = std::tuple_size<typename std::remove_reference<Tuple>::type>::value - 1>
auto my_apply(Func && func, Tuple && t) ->
decltype(my_apply_imp(std::forward<Func>(func), std::forward<Tuple>(t),
typename my_make_index_sequence<0, size>::ret{})) {
return my_apply_imp(std::forward<Func>(func), std::forward<Tuple>(t),
typename my_make_index_sequence<0, size>::ret{});
}

my_apply通过tuple_size先获取到了tuple的数量,然后my_make_index_sequence生成一个my_index_sequence序列数类,传入到my_apply_imp

从而让my_apply_imp根据生成的my_index_sequence类,匹配到S序列数

从而让std::get展开为逗号分隔的多个表达式,传递给func

可以看到,实现这个功能,关键在于C++11开始支持的,可变参数列表展开,这个功能不止是宏的_VA_ARGS__,可以按模式展开,非常强大

小结

在C++11下,完整实现需求的serverTryCatchFunc,给myInterface使用

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <cxxabi.h>
#include <memory>
#include <string>
#include <vector>

inline std::string demangle(const char* mangled) {
int status;
std::unique_ptr<char[], void (*)(void*)> result(
abi::__cxa_demangle(mangled, 0, 0, &status), std::free);
return result.get() ? std::string(result.get()) : "error occurred";
}
template <typename T>
const std::string& getType() {
static std::string type = demangle(typeid(T).name()); //使用函数内静态变量来保证每个getType模板类的RTTI调用仅运行一次
return type;
}
std::vector<std::string> split(const std::string& s, const std::string& delimiter) {
std::vector<std::string> tokens;
std::string token;
size_t prev = 0, pos = 0;
do
{
pos = s.find(delimiter, prev);
if (pos == std::string::npos) pos = s.length();
token = s.substr(prev, pos - prev);
if (!token.empty()) tokens.push_back(token);
prev = pos + delimiter.length();
}
while (pos < s.length() && prev < s.length());
return tokens;
}

template <typename T>
const std::string& getFuncName() {
static std::string func = split(getType<T>(), "::").size() >= 2 ?
split(getType<T>(), "::")[split(getType<T>(), "::").size() - 2] :
"";
return func;
}

#include <exception>
#include <iostream>
#include <tuple>

template <typename FuncType, typename... ArgsType,
typename retType = decltype(my_apply(std::declval<FuncType>(), std::declval<std::tuple<ArgsType&&...>>())),
typename std::enable_if<std::is_void<retType>::value>::type * = nullptr>
void serverTryCatchFunc(const FuncType& func, ArgsType&&... args) {
auto now = getNowMs();
auto args_tuple = std::make_tuple(args...);
auto& req = get<0>(args_tuple);
auto& rsp = get<1>(args_tuple);
try {
my_apply(func, args_tuple);
}catch (std::exception &e) {
LOG << getFuncName<FuncType>() << "|" << req <<
"|" << e.what() << "|cost: " << getNowMs() - now << endl;
}catch (...) {
LOG << getFuncName<FuncType>() << "|" << req <<
"|unknown exception" << "|cost: " << getNowMs() - now << endl;
}
LOG << getFuncName<FuncType>() << "|" << req <<
"|" << rsp << "|cost: " << getNowMs() - now << endl;
}

template <typename FuncType, typename... ArgsType,
typename retType = decltype(my_apply(std::declval<FuncType>(), std::declval<std::tuple<ArgsType&&...>>())),
typename std::enable_if<std::is_void<retType>::value>::type * = nullptr>
retType serverTryCatchFunc(const FuncType& func, ArgsType&&... args) {
retType ret;
auto now = getNowMs();
auto args_tuple = std::make_tuple(args...);
auto& req = get<0>(args_tuple);
auto& rsp = get<1>(args_tuple);
try {
ret = my_apply(func, args_tuple);
}catch (std::exception &e) {
LOG << getFuncName<FuncType>() << "|" << req <<
"|" << e.what() << "|cost: " << getNowMs() - now << endl;
}catch (...) {
LOG << getFuncName<FuncType>() << "|" << req <<
"|unknown exception" << "|cost: " << getNowMs() - now << endl;
}
LOG << getFuncName<FuncType>() << "|" << req <<
"|" << rsp << "|" << ret << "|cost: " << getNowMs() - now << endl;
return ret;
}

struct MyServer {
int myInterface(const Req &req, Rsp &rsp) override { //重写一个IDL生成的接口函数
return serverTryCatchFunc([&](const Req &req, Rsp &rsp) { //如果是C++14,使用auto&为参数的lambda就更简洁了
//业务逻辑
int ret = 0;
...
return ret;
}, req, rsp);
}
};

可以看到serverTryCatchFunc支持可变参数列表,即使超过2个参数,也可以正确传递给lambda,只打印关心的req = get<0>rsp = get<1>

甚至对serverTryCatchFunc进行微调,通过enable_if参数列表个数,可以支持仅存在req的情况