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(包 ...
随机推荐
- git报错 error: cannot stat 'file': Permission denied
切换分支(git checkout xxx)时报错: error: cannot stat 'file': Permission denied 解决方法:退出编辑器.浏览器.资源管理器等,然后再切换就 ...
- 灵雀云开源网络插件Kube-OVN 1.4.0 版发布!支持跨集群容器网络、NetworkPolicy 日志
从 1.4 开始 Kube-OVN 支持将多个 Kubernetes 集群容器网络打通,不同集群之间的 Pod 可以通过 Pod IP 直接互相通信.本版本还支持 ACL 日志,可以记录因 Netwo ...
- mysql之突破secure_file_priv写webshell
在某些情况下,当我们进入了一个网站的phpMyAdmin时,想通过select into outfile来写shell,但是通常都会报错. 这是因为在mysql 5.6.34版本以后 secure_f ...
- ffmpeg+nginx截取屏幕实时画面并进行rtmp推流在前端播放
准备环境和文件 1.下载ffmpeg的包[https://foxbaby.lanzoui.com/iYjPmup51cd] 地址:https://ffmpeg.org/download.html#bu ...
- css中两种居中方式text-align:center和margin:0 auto 的使用场景
关于使用text-align:center和margin:0 auto 两种居中方式的比较 前言:最近由于要学习后端,需要提前学习一部分前端知识,补了补css知识,发现狂神在讲这一部分讲的不是特别清楚 ...
- R语言服务器程序 Rserve详解
R语言服务器程序 Rserve详解 R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大. R语言作为统计学一门语言,一直在小众领域闪耀着光芒.直到 ...
- 《剑指offer》面试题29. 顺时针打印矩阵
问题描述 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字. 示例 1: 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4 ...
- 带你十天轻松搞定 Go 微服务系列(一)
本文开始,我们会出一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建(本文) 服务拆分 用户服务 产品服务 订单服务 支付服务 RPC 服务 Au ...
- 1013day-人口普查系统
1.shuchu.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" ...
- CMake语法—普通变量与函数(Normal Variable And Function)
目录 CMake语法-普通变量与函数(Normal Variable And Function) 1 CMake普通变量与函数示例 1.1 CMakeLists.txt 1.2 执行CMake配置脚本 ...