最近写设计模式作业的时候, 有一个作业是实现装饰器模式 (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导致的奇怪问题的更多相关文章

  1. C++ Copy Elision

    故事得从 copy/move constructor 说起: The default constructor (12.1), copy constructor and copy assignment ...

  2. Copy elision in C++

    Copy elision (or Copy omission) is a compiler optimization technique that avoids unnecessary copying ...

  3. copy elision

    http://book.51cto.com/art/200810/93007.htm 1.2.2  数据传送指令 mov:数据移动.第一个参数是目的,第二个参数是来源.在C语言中相当于赋值号.这是最广 ...

  4. 读取文本文件时<U+FEFF> 导致的奇怪问题

    项目中经常会从一些文本文件中读取数据进行业务处理,最近遇到一个问题,另外一个部门提供一个txt文本给我们进行业务处理,当我们使用字符流读取文本之后,处理时,发现第一行数据无法匹配,其他数据可以正常处理 ...

  5. copy elison & RVO & NRVO

    蓝色的博文 To summarize, RVO is a compiler optimization technique, while std::move is just an rvalue cast ...

  6. 优化代码,引发了早期缺陷导致新bug

    早期系统有个缺陷,调用js时少提交一个参数,导致该参数一直是undefined,但是不会引起bug. 对系统进行优化后,这个参数变成了必要的,然后代码一直会走else,undefined值明显不是一个 ...

  7. Docker中的Dockerfile命令详解FROM RUN COPY ADD ENTRYPOINT...

    Dockerfile指令 这些建议旨在帮助您创建高效且可维护的Dockerfile. FROM FROM指令的Dockerfile引用 尽可能使用当前的官方图像作为图像的基础.我们推荐Alpine图像 ...

  8. Copy与mutableCopy的个人理解

    Copy与mutableCopy的个人理解 1. 相同点 都是将原有对象进行深拷贝(狭义) 这里的狭义上的深拷贝指的是在不考虑编译器在编译时对不可变对象进行copy时采取的优化策略:即将不可变对象的地 ...

  9. C++11标准之右值引用(rvalue reference)

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了Copy Elision.RVO(包 ...

随机推荐

  1. 第10组 Alpha冲刺 (5/6)

    1.1基本情况 ·队名:今晚不睡觉 ·组长博客:https://www.cnblogs.com/cpandbb/p/13996848.html ·作业博客:https://edu.cnblogs.co ...

  2. 05.python解析式与生成器表达式

    解析式和生成器表达式 列表解析式 列表解析式List Comprehension,也叫列表推导式 #生成一个列表,元素0-9,将每个元素加1后的平方值组成新的列表 x = [] for i in ra ...

  3. 品味Spring Cache设计之美

    最近负责教育类产品的架构工作,两位研发同学建议:"团队封装的Redis客户端可否适配Spring Cache,这样加缓存就会方便多了" . 于是边查阅文档边实战,收获颇丰,写这篇文 ...

  4. Spring Boot Admin,贼好使!

    Spring Boot Admin(SBA)是一个开源的社区项目,用于管理和监控 Spring Boot 应用程序.应用程序可以通过 http 的方式,或 Spring Cloud 服务发现机制注册到 ...

  5. Android官方文档翻译 十四 3.2Supporting Different Screens

    Supporting Different Screens 支持不同的屏幕 This lesson teaches you to 这节课教给你 Create Different Layouts 创建不同 ...

  6. 《剑指offer》面试题57 - II. 和为s的连续正数序列

    问题描述 输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数). 序列内的数字由小到大排列,不同序列按照首个数字从小到大排列. 示例 1: 输入:target ...

  7. 2月2日 体温APP开发记录

    1.阅读构建之法 现代软件工程(第三版) 2.观看Android开发视频教程最新版 Android Studio开发 3.Edit text使用学习

  8. 【C++】STL算法

    STL算法 标签:c++ 目录 STL算法 一.不变序列算法 1.熟悉的min(), max() 2.找最值还自己动手么?不了不了 3.熟悉的find()和新学会的count() 二.变值算法 1.f ...

  9. 【记录一个问题】笔记本ThinkPad X1-Extreme安装ubuntu 18后,更新nvidia显卡驱动后出现显示问题,无法再登录

    如题 更新的过程如下: sudo ubuntu-drivers autoinstall sudo reboot 后续准备在recovery模式中尝试删除驱动.

  10. LINUX系统机器人

    简介 在2016年,国内的软硬件尚不能有效支撑我们制造智能机器人,我们无法有效在Linux进行语音唤醒,只能使用斯坦福大学狮身人面像语音开源项目来进行英文识别我们对RIMA的呼唤,抗干扰性为0,意味着 ...