加载中...
返回

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));
}
10 comments
Anonymous
Markdown is supported
@mpv945
mpv945commentedover 2 years ago

添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

@SGS4ever
SGS4evercommentedover 2 years ago

@mpv945
添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

我的博客没有使用图床,所以没办法帮到你~

@Celetherin
Celetherincommentedover 2 years ago

您好,我也是使用的stack主题,我在照着您的方法添加返回顶部按钮时,遇到了按钮虽然出现、也能够点击,但无法实现实际上的返回顶部功能的问题,我没有任何的代码知识,不知道您有没有解决方法?
另外,也是想提醒一下其他需要这篇教程的朋友,最新版的stack主题,添加返回按钮的组件应该在layouts/partials/sidebar/right.html, 在layouts/_default/single.html中添加代码会导致出现两个右边栏。

@jsjcjsjc
jsjcjsjccommentedover 2 years ago

请教一下博主,如何优雅的给stack主题添加广告哈?
我只想在左或者右侧边栏底部,或者每篇文章底部添加一个小小的广告,但是默认似乎的满屏广告哈~~
感谢

@SGS4ever
SGS4evercommentedover 2 years ago
@ClimbingMouse
ClimbingMousecommentedalmost 2 years ago

你好,按照你的方法设置页面载入动画,这个动画不会停止咋办啊

@46fafa
46fafacommentedalmost 2 years ago

博主你好,请问一下主页布局修改哪里的代码如何作用于整个网页,我发现修改后的布局只存在主页和前两篇文章,其他部分还是没修改的样子

@4kohakunushi
4kohakunushicommentedover 1 year ago

你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@SGS4ever
SGS4evercommentedover 1 year ago

首先我只能基于本文使用的Stack版本来尝试解答,因为没看过当前的Stack主题的代码~
我重新翻了下此前写的关于高亮的内容,理论上只要你的post页面的标题在menu配置中即可高亮。如果post页面是你站点的根路径,那应该可以参考我的文章里写的方法,修改下active的触发逻辑~

@4kohakunushi
你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@sansan-cc
sansan-cccommentedabout 2 months ago

感谢博主的建站帖子,有很大的帮助。

有朋自远方来,不亦说乎?