std::transform返回引用仍触发拷贝构造的原因及规避方案
前置澄清
你观察到的两次拷贝构造调用,是std::vector从初始化列表构造阶段触发的,和你调用的std::transform没有关联。注释掉std::transform相关代码后运行,输出结果完全一致。
问题1:为什么处理后的对象与原对象不是同一实例?
你产生这个误解的核心原因是没有给Math类实现自定义拷贝赋值运算符,无法观察到std::transform阶段的实际操作:
std::vector的初始化列表{ {5}, {10} }会先在栈上构造两个临时Math对象,对应输出前两行构造日志。随后vector会将这两个栈上的临时对象拷贝到自身在堆上申请的连续存储空间,触发两次拷贝构造,也就是你看到的后两行日志。此时堆上的元素和栈上的临时对象本来就是不同实例,临时对象在初始化完成后就会被销毁。std::transform的核心逻辑是对每个元素执行*输出迭代器 = 转换函数(*输入迭代器),这里输入和输出迭代器都指向v_math的已有元素,操作的都是堆上已经构造好的Math实例,只会调用拷贝赋值运算符(你没有自定义所以没有日志输出),不会创建新实例,处理前后的对象就是同一个实例,地址不会发生变化。
你可以给Math类补充拷贝赋值运算符的实现验证这一点:
Math& operator=(const Math& m) { std::cout << "Copy assignment, our address: " << this << ", his address: " << &m << std::endl; val_ = m.val_; return *this; }
补充后运行可以看到额外的两次拷贝赋值日志,赋值的源地址和目标地址完全相同,属于自赋值,没有新实例产生。
问题2:为什么拷贝目标对象的地址比源对象地址大?
你观察到的拷贝构造日志里,源地址是0x7fff开头的栈地址,目标地址是0x23xxxx开头的堆地址,实际是源地址远大于目标地址,你搞反了地址的大小关系。
这符合常规的用户态内存布局规则:栈区域处于内存空间的高地址段,且从高地址向低地址增长;堆区域处于内存空间的低地址段,从低地址向高地址增长。栈上的临时对象地址自然远高于堆上的vector元素地址。
问题3:想借std::transform实现声明式遍历vector的所有元素,要如何避免拷贝构造的发生?
std::transform的设计目的是将输入序列的元素转换后写入输出序列,天生带有赋值语义,并不适合单纯的遍历场景。要实现无拷贝的声明式遍历,正确选择是使用std::for_each:
// 只读遍历,无拷贝 std::for_each(begin(v_math), end(v_math), [](const Math& m) { // 你的遍历逻辑 }); // 可写遍历,无拷贝 std::for_each(begin(v_math), end(v_math), [](Math& m) { // 你的修改逻辑 });
std::for_each只会将每个元素的引用传入lambda执行逻辑,没有任何多余的赋值、拷贝操作,完全不会触发拷贝构造。
如果一定要强行使用std::transform实现遍历,可以传入无操作的输出迭代器,但属于对算法的滥用,完全不推荐。
内容的提问来源于stack exchange,提问作者glades
相关产品推荐
相关产品推荐

