本文主要是介绍一些个人开发过程中遇到的奇怪代码,奇怪并非指代代码错误或写法低效,单纯个人知识盲区
# 杂项
# new 构造函数的非零构造
一般调用构造函数的时候,都会初始化对象里的变量,如果不指定的情况下就会执行零构造,对于以下代码,相比大家都很熟悉:
class CMyClass | |
{ | |
int c; | |
} | |
CMyClass a = new CMyClass(); | |
// a.c == 0 |
初始化一个类对象,但其中实际还隐含了一个操作 —— 值初始化。这一步实际上会把 CMyClass
中的成员 c
初始化为 0,并且初始化该类的虚函数表等其他内容,那有什么办法可以只初始化虚函数表等关系,但是不初始化值呢?答案是有的,而且我还撞见了🤔
CMyClass a = new CMyClass(); | |
a.c = 100; | |
/* 如果使用 CMyClass (),则会进行值初始化 */ | |
new (a) CMyClass(); | |
// a.c == 0 | |
/* 如果只是用 CMyClass,则不会进行值初始化,c 依然是 100 */ | |
new (a) CMyClass; | |
// a.c == 100 |
是不是很神奇,但是有一点要注意,这种不进行值初始化的操作,必须避免在无参构造函数内进行值初始化,不然 new (a) CMyClass
依旧会执行无参构造函数内的逻辑,是否会用初始化值覆盖数据取决于构造函数内部的逻辑。
# map 的默认插入
例如下面这段代码,在执行 map 的 operate[]
操作的时候,涉及到一个默认值的概念,如果 key 不存在的情况下会构造一个默认的 v,并连同 k 一起插入 map。
这种写法可以简化赋值和初始化的工程,有点类似 python 的 defaultdict
:
#include <iostream> | |
#include <map> | |
using namespace std; | |
struct A | |
{ | |
int m; | |
}; | |
int main() | |
{ | |
std::map<int, std::map<int, A>> m_a; | |
m_a[2][1]; // 插入默认的 kv | |
A& a = m_a[1][0]; // 插入默认 kv,并返回 val 的引用 | |
// 直接拿来用... | |
a.m=2; | |
return 0; | |
} |
下面是 operate[]
函数的定义,和原理
mapped_type& | |
operator[](const key_type& __k) | |
{ | |
// concept requirements | |
__glibcxx_function_requires(_DefaultConstructibleConcept<mapped_type>) | |
iterator __i = lower_bound(__k); // 这里查找一个 k >= __k 的位置用来进行插入 | |
// __i->first is greater than or equivalent to __k. | |
if (__i == end() || key_comp()(__k, (*__i).first)) | |
#if __cplusplus >= 201103L | |
__i = _M_t._M_emplace_hint_unique(__i, std::piecewise_construct, | |
std::tuple<const key_type&>(__k), | |
std::tuple<>()); | |
#else | |
__i = insert(__i, value_type(__k, mapped_type())); | |
#endif | |
return (*__i).second; | |
} | |
//_M_emplace_hint_unique 定义 | |
template<typename _Key, typename _Val, typename _KeyOfValue, typename _Compare, typename _Alloc> | |
template<typename... _Args> | |
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator | |
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>:: _M_emplace_hint_unique(const_iterator __pos, _Args&&... __args) | |
{ | |
_Link_type __z = _M_create_node(std::forward<_Args>(__args)...); | |
__try | |
{ | |
// 查询合法的插入点 | |
auto __res = _M_get_insert_hint_unique_pos(__pos, _S_key(__z)); | |
// 有合法的插入点,执行插入 | |
if (__res.second) | |
return _M_insert_node(__res.first, __res.second, __z); | |
// 已经存在了,就直接返回 | |
_M_drop_node(__z); | |
return iterator(__res.first); | |
} | |
__catch(...) | |
{ | |
_M_drop_node(__z); | |
__throw_exception_again; | |
} | |
} |
首先我们要知道 C++ map 的实现是基于红黑树,红黑树本身是一棵平衡二叉树,根节点大于左子树小于右子树。
插入的核心 _M_get_insert_hint_unique_pos
,主要功能是查询 key 对应的位置的前节点和后节点,来控制到底是插入在后节点左侧还是右侧:
template<typename _Key, typename _Val, typename _KeyOfValue, typename _Compare, typename _Alloc> | |
pair<typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr, | |
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr> | |
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>:: _M_get_insert_hint_unique_pos(const_iterator __position, const key_type &__k) | |
{ | |
iterator __pos = __position._M_const_cast(); | |
typedef pair<_Base_ptr, _Base_ptr> _Res; | |
// 等于 _M_end 可以理解为这棵树只有根节点,可能有叶节点 | |
if (__pos._M_node == _M_end()) | |
{ | |
// 有叶节点,并且 key 比最右叶节点还大 | |
if (size() > 0 && _M_impl._M_key_compare(_S_key(_M_rightmost()), __k)) | |
{ | |
// 插入点在 _M_rightmost () 右边 | |
return _Res(0, _M_rightmost()); | |
} | |
else | |
{ | |
// 中间位置做二分查找定位插入点 | |
return _M_get_insert_unique_pos(__k); | |
} | |
} | |
// __k < pos | |
else if (_M_impl._M_key_compare(__k, _S_key(__pos._M_node))) | |
{ | |
iterator __before = __pos; | |
// 如果 pos 已经是最左节点 | |
if (__pos._M_node == _M_leftmost()) | |
{ | |
// 插入点在 _M_leftmost () 左边 | |
return _Res(_M_leftmost(), _M_leftmost()); | |
} | |
// pre_pos < __k < pos | |
else if (_M_impl._M_key_compare(_S_key((--__before)._M_node), __k)) | |
{ | |
//pre_pos 没有右子树 | |
if (_S_right(__before._M_node) == 0) | |
{ | |
// 插入点在 pre_pos 右边 | |
return _Res(0, __before._M_node); | |
} | |
else | |
{ | |
// 插入点在 pos 左边 | |
return _Res(__pos._M_node, __pos._M_node); | |
} | |
} | |
else | |
{ | |
// 中间位置做二分查找定位插入点 | |
return _M_get_insert_unique_pos(__k); | |
} | |
} | |
// pos < k | |
else if (_M_impl._M_key_compare(_S_key(__pos._M_node), __k)) | |
{ | |
iterator __after = __pos; | |
// 如果 pos 已经是最右节点 | |
if (__pos._M_node == _M_rightmost()) | |
{ | |
// 插入点在 pos 的右边 | |
return _Res(0, _M_rightmost()); | |
} | |
// pos < __k < after_pos | |
else if (_M_impl._M_key_compare(__k, _S_key((++__after)._M_node))) | |
{ | |
//pos 没有右子树 | |
if (_S_right(__pos._M_node) == 0) | |
{ | |
// 插入点在 pos 的右边 | |
return _Res(0, __pos._M_node); | |
} | |
else | |
{ | |
// 插入点在 after_pos 的左边 | |
return _Res(__after._M_node, __after._M_node); | |
} | |
} | |
else | |
{ | |
// 中间位置做二分查找定位插入点 | |
return _M_get_insert_unique_pos(__k); | |
} | |
} | |
else | |
{ | |
// 查询到结果直接返回 res.second == 0 表示不插入 | |
return _Res(__pos._M_node, 0); | |
} | |
} |
# piecewise_construct 和 tuple<>()
piecewise_construct
类型主要是用于区分构造函数在构造时,参数混淆的问题。见上面 map 的构造,k 和 v 都可能是一个可变长参数,那么如果传递一个可变长的参数交由 k,v 自己去识别该从哪里开始进行截断,未免太劳烦编译器,因此 piecewise_construct
可以通过后置两个 tuple
的形式来完成参数的隔离,更详细的说明可以参考该文档:
// 对于 map 中 node 的构造 | |
//std::piecewise_construct 用于申明分隔模式 | |
//std::tuple<const key_type&>(__k) 表示构造 k 的参数 | |
//std::tuple<>() 表示构造 v 的参数 | |
_M_t._M_emplace_hint_unique(__i, std::piecewise_construct, std::tuple<const key_type&>(__k), std::tuple<>()); |
那么还有一个问题: tuple<>()
是个什么妖魔鬼怪。下面来看一下它的定义:
// Explicit specialization, zero-element tuple. | |
template<> | |
class tuple<> | |
{ | |
public: | |
void swap(tuple &) noexcept | |
{ /* no-op */ } | |
// We need the default since we're going to define no-op | |
// allocator constructors. | |
tuple() = default; | |
// No-op allocator constructors. | |
template<typename _Alloc> | |
tuple(allocator_arg_t, const _Alloc &) | |
{} | |
template<typename _Alloc> | |
tuple(allocator_arg_t, const _Alloc &, const tuple &) | |
{} | |
}; |
zero-element tuple,顾名思义,一个「空元组」,它是 tuple<T>
的一个特化,这里我猜想应该是为了用无参的默认构造函数来初始化 val,而专门传进去的一个「空元组」来 “凑数” 的~
# C++ 中的 POD
POD 是什么还得从一段 PhysX 代码说起,PhysX 自带的 Array 结构里有一个 popBack 操作,会根据 pop 的内容判断是否调用析构函数:
PX_INLINE T popBack() | |
{ | |
PX_ASSERT(mSize); | |
T t = mData[mSize - 1]; | |
if(!isArrayOfPOD()) | |
{ | |
mData[--mSize].~T(); | |
} | |
else | |
{ | |
--mSize; | |
} | |
return t; | |
} | |
PX_FORCE_INLINE static bool isArrayOfPOD() | |
{ | |
#if PX_LIBCPP | |
return std::is_trivially_copyable<T>::value; | |
#else | |
return std::tr1::is_pod<T>::value; | |
#endif | |
} |
std::tr1::is_pod
便是用来判断一个对象是否为 POD 对象。那么怎么理解什么是 POD 对象呢?
大致就是一个类如果可以用 struct 或者 union 代替,其只有数据定义,没有任何函数时那么就可以称之为一个 POD 对象。