C++的Copy Elision导致的奇怪问题
最近写设计模式作业的时候, 有一个作业是实现装饰器模式 (Decorator Pattern), 由于我不会 Java, 所以只能用 C++ 来实现
在这个背景下, 会有简单(表意)的几个类, 如下:
class Base
{
public:
virtual ~Base() = 0;
virtual int getData() const = 0;
};
inline Base::~Base() {}
class DerivedA final : public Base
{
public:
DerivedA(int data) : data_(data) {}
int getData () const override
{
return data_;
}
private:
const int data_;
};
class DerivedB final : public Base
{
public:
DerivedB(const Base &pre) : pre_(pre) {}
int getData () const override
{
return pre_.getData() + 1;
}
private:
const Base &pre_;
};
简单来写就是上面这样, DerivedB 类型的对象可以接收以 Base 类作为基类的对象引用并且绑定到成员 pre_ 上, 在调用 getData 方法时会调用 pre_ 绑定的对象的 getData 方法, 并在其结果的基础上运算后返回
而这样的设计会导致一种很直觉的使用方法, 如下:
cout << DerivedB(DerivedB(DerivedA(10))).getData() << endl;
也即嵌套对象, 实现 getData 的多次调用
但是这样的使用方式会造成与预期不符的结果出现, 如下:
~> g++ -std=c++17 test.cpp -o test
~> ./test
11
~> clang++ -std=c++17 test.cpp -o test
~> ./test
11
会发现结果表明, 只有一个 DerivedB 类型的对象被构造了出来, cppreference 的 copy elision 章节中的解释部分有提到:
Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible, as the language rules ensure that no copy/move operation takes place, even conceptually:
- In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:
T f() {
return T();
} f(); // only one call to default constructor of T
- In the initialization of a variable, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:
T x = T(T(f())); // only one call to default constructor of T, to initialize x
上面这句被我标粗的文字可以看到, 即使拷贝/移动构造有副作用, 依然只构造一次, 甚至不需要有拷贝/移动构造函数
可以在类中添加如下定义:
class DerivedB final : public Base
{
public:
// new additions
DerivedB(const DerivedB &) = delete;
DerivedB(DerivedB &&) = delete;
DerivedB(const Base &pre) : pre_(pre) {}
int getData () const override
{
return pre_.getData() + 1;
}
private:
const Base &pre_;
};
会发现依然可以通过编译并且运行结果与之前相同 ( 因为在 C++17 中 Copy Elision 已经不再是可选项 ), 但是在 C++17 之前如果 delete 了这两个拷贝/移动构造函数, 会导致无法通过编译, 尽管有可以匹配 const Base & 类型的构造函数, 也依然不可以:
test.cpp:45:13: error: functional-style cast from 'DerivedB'
to 'DerivedB' uses deleted function
cout << DerivedB(DerivedB(DerivedA(10))).getData...
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.cpp:31:5: note: candidate constructor has been
explicitly deleted
DerivedB(DerivedB &&) = delete;
^
test.cpp:30:5: note: candidate constructor has been
explicitly deleted
DerivedB(const DerivedB &) = delete;
^
test.cpp:33:5: note: candidate constructor
DerivedB(const Base &pre) : pre_(pre) {}
^
1 error generated.
感谢 禽牙 在评论中提出, 事实上如果我们在尚未 delete 那两个构造函数通过如下的方式调用, 也依然不可行:
DerivedA a(10);
DerivedB b1(a);
DerivedB b2(b1);
cout << b2.getData() << endl;
结果依然是 11, 这是因为重载决议后其实我们调用的是匹配类型为 const DerivedB & 的构造函数, 同样如果我们约定在代码中都是用这样的方式编写程序, 我们就可以获得一种解决方式, 编写匹配类型的构造函数, 如下:
...
DerivedB(const DerivedB &pre) : pre_(pre) {}
...
再通过声明中间变量的方式调用就可以获得正确结果, 但是通过纯右值的形式依然不行:
...
DerivedA a(10);
DerivedB b1(a);
DerivedB b2(b1);
cout << b2.getData() << endl;
// print 12
...
cout << DerivedB(DerivedB(DerivedA(10))).getData() << endl;
// print 11
...
所以如何才能在这种设计下通过这种方式正常使用呢?
一种可以显著增加代码程度的方式是, 手动添加强制转换:
cout << DerivedB(static_cast<const Base &>(DerivedB(DerivedA(10)))).getData() << endl;
cout << DerivedB((const Base &)(DerivedB(DerivedA(10)))).getData() << endl; // C-style
当然这种方式其实还是可以接受的
还有一种可以不会增加太多冗余代码的方式是在构造函数里增加一个冗余参数, 区分开就可以了, 比如:
...
class DerivedB final : public Base
{
public:
DerivedB(const Base &pre, int) : pre_(pre) {}
int getData () const override
{
return pre_.getData() + 1;
}
private:
const Base &pre_;
};
...
cout << DerivedB(DerivedB(DerivedA(10), 0), 0).getData() << endl;
当然以上两种都是在不涉及模板的情况下完成的, 但是都会对调用的方便性产生影响
还有一种不会更改调用方式, 通过模板区分嵌套的相同类型的方式, 感谢 91khr 提供:
首先先将 DerivedB 变成一个类模板, 之后添加模板推导指引来实现嵌套区分
...
template <int Lv>
class DerivedB;
...
template <typename Ty> DerivedB(Ty) -> DerivedB<0>;
template <int Lv> DerivedB(DerivedB<Lv>) -> DerivedB<Lv + 1>;
...
cout << DerivedB(DerivedB(DerivedA(10))).getData() << endl;
在此条件之下, 结果与预期相同, 完美解决问题:
~> clang++ -std=c++17 test.cpp -o test
~> ./test
12
C++的Copy Elision导致的奇怪问题的更多相关文章
- C++ Copy Elision
故事得从 copy/move constructor 说起: The default constructor (12.1), copy constructor and copy assignment ...
- Copy elision in C++
Copy elision (or Copy omission) is a compiler optimization technique that avoids unnecessary copying ...
- copy elision
http://book.51cto.com/art/200810/93007.htm 1.2.2 数据传送指令 mov:数据移动.第一个参数是目的,第二个参数是来源.在C语言中相当于赋值号.这是最广 ...
- 读取文本文件时<U+FEFF> 导致的奇怪问题
项目中经常会从一些文本文件中读取数据进行业务处理,最近遇到一个问题,另外一个部门提供一个txt文本给我们进行业务处理,当我们使用字符流读取文本之后,处理时,发现第一行数据无法匹配,其他数据可以正常处理 ...
- copy elison & RVO & NRVO
蓝色的博文 To summarize, RVO is a compiler optimization technique, while std::move is just an rvalue cast ...
- 优化代码,引发了早期缺陷导致新bug
早期系统有个缺陷,调用js时少提交一个参数,导致该参数一直是undefined,但是不会引起bug. 对系统进行优化后,这个参数变成了必要的,然后代码一直会走else,undefined值明显不是一个 ...
- Docker中的Dockerfile命令详解FROM RUN COPY ADD ENTRYPOINT...
Dockerfile指令 这些建议旨在帮助您创建高效且可维护的Dockerfile. FROM FROM指令的Dockerfile引用 尽可能使用当前的官方图像作为图像的基础.我们推荐Alpine图像 ...
- Copy与mutableCopy的个人理解
Copy与mutableCopy的个人理解 1. 相同点 都是将原有对象进行深拷贝(狭义) 这里的狭义上的深拷贝指的是在不考虑编译器在编译时对不可变对象进行copy时采取的优化策略:即将不可变对象的地 ...
- C++11标准之右值引用(rvalue reference)
1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了Copy Elision.RVO(包 ...
随机推荐
- iView 用renderContent自定义树组件
iview的树组件在有默认选中状态的时候默认选中状态的样式改变有bug,默认选中的样式不好看,鉴于此,有renderContent来改造iview的树组件, 效果如图 代码如下 <templat ...
- 【Spring专场】「AOP容器」不看源码就带你认识核心流程以及运作原理
前提回顾 前一篇文章主要介绍了spring核心特性机制的IOC容器机制和核心运作原理,接下来我们去介绍另外一个较为核心的功能,那就是AOP容器机制,主要负责承接前一篇代理模式机制中动态代理:JDKPr ...
- 网络协议学习笔记(七)流媒体协议和P2P协议
概述 上一篇讲解了http和https的协议的相关的知识,现在我们谈一下流媒体协议和P2P协议. 流媒体协议:如何在直播里看到美女帅哥 最近直播比较火,很多人都喜欢看直播,那一个直播系统里面都有哪些组 ...
- 互联网医疗行业PEST分析实践
前言 今年开始逐步切入产品与数据工作,完全脱离了原来的舒适区,确实有一些挑战.开始以为只做数仓建设的事情,就仓促的学习了一些数仓相关的知识,但没搞多久,还要负责公司BI的工作,又开始补习数分相关的知识 ...
- 单片机main函数退出后发生什么——以stm32为例
STM32:main函数退出后发生什么? 我们都在说单片机要运行在无限循环里,不能退出,可退出之后会发生什么? 讨论STM32启动过程的文章数不胜数,可main函数结束之后会发生什么却少有讨论. 几日 ...
- 记录ABAP开发的日常——SAP_PO开发同步接口案例
前言:在项目中遇到任务PO接口,需求是SRM发送采购订单信息给SAP,SAP根据信息调用BAPI同步数据,在此作为案例记录. 本次接口采用的协议是SOAP,当然也有其他的协议比如REST等等,在此不做 ...
- javaObject类—getClass方法
1 package face_object; 2 /* 3 * Object:所有类的根类. 4 * Object是不断抽取而来的,具备所有对象都具备的共性内容. 5 * 常用的共性功能: 6 * 7 ...
- nmap 查看主机上开放的端口
作用: 检测网络上的主机检测主机上开放的端口检测操作系统,硬件地址,以及软件版本检测脆弱性的漏洞(Nmap的脚本) 扫描方式: 1. -sS Tcp SYN Scan 不需要三次握手,速度快 ...
- Error: xz compression not available解决办法
centos6升级php时误安装: rpm -Uvh https://mirror.webtatic.com/yum/el7/epel-release.rpm rpm -Uvh https://mir ...
- threejs - src - WebGLProgram是如何组建Shader的?
threejs - src - WebGLProgram是如何组建Shader的? WebGLProgram的构建 WebGLProgram构建的时候需要的参数如下: // \param render ...