CyberRt源码剖析-常用宏分析
从这篇blog开始,我们来分析百度CyberRt这个高性能的分布式通信中间件
CyberRt的源码组成如下:
base
文件夹是Apollo开发的高性能基础库,我们先从这个文件夹里的代码看起:
从文件命名中可以看见实现了与线程相关的如线程池、锁、无锁队列、哈希表等等基础组件,我们就开始从零造轮子吧
1.c++前置知识
std::nothrow
(std::nothrow)
是在C++中用于进行内存分配时的一种选项。通常,当你使用 new
运算符创建对象时,如果内存分配失败,new
会抛出 std::bad_alloc
异常。但是,当你希望在分配失败时不抛出异常,而是返回一个空指针,你可以使用 (std::nothrow)
作为参数传递给 new
。
具体来说,使用 (std::nothrow)
会使得 new
在分配失败时返回一个空指针而不是抛出异常。这样,你可以在分配失败时通过检查返回的指针是否为空来处理错误,而不必使用异常处理机制。
以下是一个示例:
|
在这个例子中,我们尝试分配一个非常大的整数数组。由于这个数组可能太大而无法成功分配,我们使用 (std::nothrow)
,这样 new
在分配失败时会返回一个空指针。在分配失败的情况下,我们打印一条错误消息。这样,我们可以通过检查指针是否为空来处理内存分配失败的情况,而不必处理异常。
std::once_flag && std::call_once
在C++中,std::once_flag
是一个用于确保只执行一次代码的标记。它通常与 std::call_once
函数一起使用,以确保其中的代码只会在多线程环境下被执行一次。
多线程环境中,多个线程可能同时尝试执行某个特定的代码块,但有些代码块可能只需要执行一次。这时,就可以使用 std::once_flag
和 std::call_once
来确保代码块只会在第一次调用时执行,而后续调用会被忽略。
在C++中,std::once_flag
是一个用于确保只执行一次代码的标记。它通常与 std::call_once
函数一起使用,以确保其中的代码只会在多线程环境下被执行一次。
多线程环境中,多个线程可能同时尝试执行某个特定的代码块,但有些代码块可能只需要执行一次。这时,就可以使用 std::once_flag
和 std::call_once
来确保代码块只会在第一次调用时执行,而后续调用会被忽略。
template<typename Callable, typename... Args> |
在这个简化的示例中,std::once_flag
包含一个互斥锁(mutex
)和一个布尔标志(called
)。当第一个线程调用 std::call_once
时,它会获得互斥锁,检查标志。如果标志为假,表示函数还没有执行过,于是调用传入的函数,然后设置标志为真,释放互斥锁。如果标志为真,说明函数已经执行过,不再重复执行。
通过使用互斥锁和双检锁的技术,std::call_once
在多线程环境下能够保证传入的函数只执行一次,同时尽可能地减小了锁的开销。需要注意的是,尽管双检锁可以提高性能,但也需要小心处理一些细节,以防止出现竞态条件和内存可见性的问题。在实践中,使用现代C++标准库提供的 std::call_once
是比手动实现更为安全和简便的选择。
std::enable_if
typename std::enable_if<!HasShutdown<T>::value>::type |
这行代码使用了std::enable_if
,它是一个模板元编程工具,用于在编译时根据条件启用或禁用模板的某个部分。
std::enable_if<HasShutdown<T>::value>
:这是一个模板元编程的条件,它基于HasShutdown<T>::value
的值。如果HasShutdown<T>::value
为true
,那么这个表达式的结果是std::enable_if
的一个特殊的内部类型,否则没有这个内部类型。typename
:这是告诉编译器,后面的std::enable_if<HasShutdown<T>::value>::type
是一个类型名,而不是一个成员变量或函数。
所以,整个表达式的意思是:如果 HasShutdown<T>::value
为 true
,则这是一个有效的类型;否则,这个表达式没有有效的类型。
2.宏分析
2.1 特征判断宏
DEFINE_TYPE_TRAIT
定义在base/macros.h
中:
DEFINE_TYPE_TRAIT(name, func)
定义了一个宏,该宏接受两个参数,name
是要定义的类型特征结构体的名称,func
是要检查的成员函数的名称。这个宏在内部定义了一个名为name
的结构体,此结构体中提供了两个函数模板。template <typename T> struct name { ... };
定义了一个模板结构体,该结构体接受一个类型参数T
。实际上这个宏就是用来检查传入的这个T
是否包含有func
这个函数,借助了name
这个结构体来实现name
结构体的内部有一个数据成员就是value
,value
的类型是static constexpr bool
,是一个静态常量,这里使用constexpr
来声明此变量是为了让编译器在编译的时候就把value
的值计算出来,value
的值就代表了传入的这个T
是否有func
这个函数,这个值的计算是通过调用Test<T>
这个函数模板来实现的,传入的参数是nullptr
Test<T>
这个函数模板有一个泛化的版本和一个特化的版本,首先来看特化的版本template <typename Class> \
static constexpr bool Test(decltype(&Class::func)*) { \
return true; \
}通过
decltype
去识别&Class::func
,如果说&Class::func
是存在的话,那么decltype(&Class::func)
推断出的类型就是一个指向成员函数函数的指针,这就说明了class
存在一个名为func
的函数,后面这个*
号我没想明白为啥要加估计是为了告诉编译器传入的参数是一个指针类型吧,如果是这样的话代码可以改成下面这个样子更具有可读性:template <typename Class> \
static constexpr bool Test(decltype(&Class::func)* ptr) { \
return true; \
}这样也就解释了为啥后面计算
value
值得时候传入了一个nullptr
作为参数。回到最初,假设我现在传入的这个T
这个类没有func
这个成员,那么编译器就会去调用下面这个泛化的版本,并且上面那个特化的版本不会报错,这叫做c++的SFINAE
特性template <typename> \
static constexpr bool Test(...) { \
return false; \
}
当你想要检查一个类型是否有某个成员函数时,你可以使用这个宏。下面是一个简单的例子,假设你想检查一个类型是否有 size
成员函数:
|
在这个例子中,DEFINE_TYPE_TRAIT(HasSize, size)
定义了一个名为 HasSize
的类型特征结构体,用于检查类型是否有 size
成员函数。然后,通过 HasSize<MyClass>::value
和 HasSize<AnotherClass>::value
分别检查了 MyClass
和 AnotherClass
是否有 size
成员函数。根据定义,MyClass
有 size
成员函数,而 AnotherClass
没有,因此输出结果分别为 1
(true)和 0
(false)。
2.2 单例宏
代码在:cyber/common/macros.h
中:
DEFINE_TYPE_TRAIT(HasShutdown, Shutdown) |
首先定了两个很有意思的函数模板
DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)
template <typename T>
typename std::enable_if<HasShutdown<T>::value>::type CallShutdown(T *instance) {
instance->Shutdown();
}
template <typename T>
typename std::enable_if<!HasShutdown<T>::value>::type CallShutdown(
T *instance) {
(void)instance;
}DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)
结合上面所讲的这个宏定义了一个名为HasShutdown
的结构体用于判断传入的模板T
这个类是否包含Shutdown
这个成员函数,然后又使用了c++的SFINAE
特性做了一个很有意思的操作typename
告诉了编译器后面std::enable_if<HasShutdown<T>::value>::type
这玩意儿是一个类型,上面这两个CallShutdown
的函数模板入参都是一样的,唯一有区别的就是这个返回值的类型。我们来看看
std::enable_if
的源码// Primary template.
/// Define a member typedef @c type only if a boolean constant is true.
template<bool, typename _Tp = void>
struct enable_if
{ };
// Partial specialization for true.
template<typename _Tp>
struct enable_if<true, _Tp>
{ typedef _Tp type; };可以看见c++官方对
std::enable_if
做了两种定义,一个泛化版本和一个特化版本std::enable_if<true, T> //用的是偏特化的版本
std::enable_if<false, T> // 用的是泛化的假设
HasShutdown<T>::value
的值为true
,那么上面两个CallShutdown
函数就变成了如下两种情况:typename std::enable_if<true>::type
typename std::enable_if<false>::type很明显
std::enable_if<false>::type
会走泛化版本,此时std::enable_if
内部是没有type
这个成员的,由于SFINAE
特性的存在,此时并不会报错,所以编译器只会去走std::enable_if<true, T>
,但是这里我很奇怪的一点是typename std::enable_if<true>::type
这里没有传第二模板参数呀,按我的理解是不是应该``typename std::enable_if<true,T>::type这样子用,我不是很明白,我想了一下,我先姑且认为编译器会先去做泛化版本的判断,此时由于泛化版本中
template<bool, typename _Tp = void>,第二模板参数被默认成了
void此时编译器发现前面这个bool值为
true然后就去走下面这个特化版本,在特化版本中就会定义
type的值,此时就是:
typedef void type`了。同理假设
HasShutdown<T>::value
的值为fasle
,上面两个CallShutdown
函数就反过来了,当去调用CallShutdown
函数时就会走下面这个版本。
综上所述,上面这两个函数模板就是用于检测类型
T
具有Shutdown
成员函数,那么这个函数将被启用,否则将被禁用。在启用的情况下,它调用instance->Shutdown()
。类型T
不具有Shutdown
成员函数的情况。在这种情况下,这个函数什么都不做。DISALLOW_COPY_AND_ASSIGN
宏用于禁止拷贝和赋值操作。它通过将拷贝构造函数和拷贝赋值操作符声明为delete
来阻止对象的拷贝和赋值。这通常用于单例模式等情况,以确保对象只能有一个实例。DECLARE_SINGLETON
宏用于声明一个单例模式的类。具体来说,它包含以下功能:Instance(bool create_if_needed = true)
函数用于获取单例对象的实例。如果单例对象尚未创建,它将使用std::call_once
和new
运算符创建一个实例。这确保在多线程环境下仅执行一次对象的创建操作。CleanUp()
函数用于清理单例对象。它调用CallShutdown
函数来执行对象的清理操作。classname()
构造函数声明为private
,确保类的实例只能通过Instance
函数创建。DISALLOW_COPY_AND_ASSIGN(classname)
用于禁止拷贝和赋值操作,将默认构造也声明成私有的,以确保类的唯一性。