加载中...
返回

C++「emplace」的些许细节

较新版本的cpp容器支持一些 emplace 操作,比如 vector::empalcemap::emplace ,其原地构造特性对于一些拷贝成本较高的对象容器来说着实吸引人。

不过 emplace 动作有些时候会退化回拷贝,近期恰好有需求要用到拷贝成本高的对象,希望在它们的容器中善用 emplace 来节约开销,因此在业余时间浅做了一些实验,希望这些认知可以指导后续的实践。

1 emplace左值——退化为拷贝

为了便于观察对象的移动/拷贝,简单实现一个对象(经典做法):

class Obj {
public:
    Obj() { std::cout << "ctor" << std::endl; }
    ~Obj() { std::cout << "dtor" << std::endl; }
    Obj(const Obj &rhs) { std::cout << "copy ctor" << std::endl; }
    Obj(Obj &&rhs) { std::cout << "move ctor" << std::endl; }
    Obj &operator=(const Obj &rhs) { std::cout << "copy operator=" << std::endl; return *this; }
    Obj &operator=(Obj &&rhs) { std::cout << "move operator=" << std::endl; return *this; }
    void Echo() { std::cout << "Hello world" << std::endl; }
};

对于通常的业务代码,容器一般是封装在某个类中的,在这里为了更加还原实际场景,简单设置一个 Shelter 类,其成员是一个 multimap ,提供一个对外的 Push 接口来往 multimap 中添加数据,然后在 Push 接口里调用 multimap::emplace 方法。

class Shelter {
public:
    void Push(std::pair<int, Obj> item);
    void Dump();

private:
    std::multimap<int, Obj> mmp_;
};


void Shelter::Push(std::pair<int, Obj> item)
{
    std::cout << "Shelter::Push =========>" << std::endl;
    mmp_.emplace(item);
}

void Shelter::Dump()
{
    for (auto &[k, v] : mmp_) {
        v.Echo();
    }
}

1.1 传值

上面给出的 Shelter 实现中, Push 参数按值传递。

main 函数分别向 Push 接口传一个右值和一个左值:

int main(int argc, const char * argv[])
{
    Shelter shelter;
    shelter.Push({1, Obj()});

    auto input = std::make_pair(2, Obj());
    shelter.Push(input);
    
    shelter.Dump();
    return 0;
}

观察输出:

ctor
move ctor
Shelter::Push =========>
copy ctor
dtor
dtor
ctor
move ctor
dtor
copy ctor
Shelter::Push =========>
copy ctor
dtor
Hello world
Hello world
dtor
dtor
dtor

分析程序分别执行了什么动作导致这些输出的产生:

# shelter.Push({1, Obj()});
ctor						# 临时对象创建
move ctor					# 临时对象移动到pair
Shelter::Push =========>
copy ctor					# emplace,产生复制
dtor						# Push参数对象析构
dtor						# pair里的临时对象析构

# auto input = std::make_pair(2, Obj());
# shelter.Push(input);
ctor						# make_pair临时Obj创建
move ctor					# 临时Obj移动到pair
dtor						# 临时Obj销毁
copy ctor					# 值传递
Shelter::Push =========>
copy ctor					# emplace,复制
dtor						# Push参数对象析构

# shelter.Dump()
Hello world
Hello world

# 本地obj对象和shelter.mmp_里面的两个对象析构
dtor
dtor
dtor

可见,对于一个纯粹的左值来说, emplace 会退化成为拷贝构造,显然不太符合实际业务中原地构造、节省开销的诉求。

1.2 传左值/右值引用

简单改写一下 Push 接口,使其可以区分右值和左值:

void Shelter::Push(std::pair<int, Obj> &item)
{
    std::cout << "Shelter::Push lvalue ref =========>" << std::endl;
    mmp_.emplace(item);
}

void Shelter::Push(std::pair<int, Obj> &&item)
{
    std::cout << "Shelter::Push rvalue ref =========>" << std::endl;
    mmp_.emplace(item);
}

输出却毫无变化:

# 以下只关注Push内部发生的事情,外部其他构造/析构输出省略
Shelter::Push rvalue ref =========>
copy ctor		# emplace,还是拷贝

Shelter::Push lvalue ref =========>
copy ctor

诶?怎么传入右值和左值,都在 emplace 的时候拷贝?注意:右值引用本身是一个左值类型,因此本质上传给 emplace 的还是一个左值,为了在 Push(Obj &&) 接口内充分利用移动语义,需要再执行一回类型转换:

void Shelter::Push(std::pair<int, Obj> &&item)
{
    std::cout << "Shelter::Push rvalue ref =========>" << std::endl;
    mmp_.emplace(std::move(item));
}
# 调用方法仍旧如前
Shelter::Push rvalue ref =========>
move ctor		# 符合预期,入参是右值,移动

Shelter::Push lvalue ref =========>
copy ctor		# 入参是左值,拷贝

2 减少重复代码

实际上分别实现了参数类型为左值引用和右值引用的 Push 后,我们已然能够借助移动语义来提高程序性能,不过,从代码上看,却发现这两个版本大差不差,令人很想将它们合并起来。

自动区分入参是左值还是右值,并原原本本传递给 multimap::emplace ,这听着真是一个再熟悉不过的场景了,简直是 万能引用 + 完美转发 的模板场景:

template<typename T>
void Push(T &&val)
{
    std::cout << "Shelter::Push =========>" << std::endl;
    mmp_.emplace(std::forward<T>(val));
}

坏处是由于模板参数没法自动推导 {xxx} 的类型,之前向接口传入右值的写法必须改写:

shelter.Push({1, Obj()}); // 编译错误

shelter.Push(std::make_pair(1, Obj())); // fine

输出则是符合预期的:

Shelter::Push =========>
move ctor		# 入参是右值,传给emplace的就是右值

Shelter::Push =========>
copy ctor		# 入参是左值,传给emplace的就是左值

如果希望对接口的自由度进行进一步限制,也可以只允许 T 被推导为 pair<int, Obj>

using MapItem = std::pair<int, Obj>;
template<typename T, std::enable_if_t<std::is_same_v<std::remove_reference_t<T>, MapItem>, int> = 0>
void Push(T &&val)
{
    std::cout << "Shelter::Push =========>" << std::endl;
    mmp_.emplace(std::forward<T>(val));
}
有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy