本文主要是介绍一些个人开发过程中遇到的奇怪代码,奇怪并非指代代码错误或写法低效,单纯个人知识盲区

# 杂项

# 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 的形式来完成参数的隔离,更详细的说明可以参考该文档

image-20220526130238160

// 对于 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 对象呢?

image-20220719112117115

大致就是一个类如果可以用 struct 或者 union 代替,其只有数据定义,没有任何函数时那么就可以称之为一个 POD 对象。

更新于 阅读次数

请我[恰饭]~( ̄▽ ̄)~*

鑫酱(●'◡'●) 微信支付

微信支付

鑫酱(●'◡'●) 支付宝

支付宝