最近写设计模式作业的时候, 有一个作业是实现装饰器模式 (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. git 不小心把某个文件给 add 了 的解决方法

    1.我不小心把这两个文件给add 进来本地仓库 2.解决 进入指令框 ,执行 git rm --cached  文件名 如下图 注意,必须指定文件否则会删除所有

  2. mysql数据库主从复制教程

    mysql主从复制教程 架构规划: 192.168.201.150 master 主节点 192.168.201.154 slave 从节点 1. 修改mysql的配置文件(主节点,从节点都要修改) ...

  3. axios导出 excel

    this.axios({ methods: 'get', url: url, responseType: 'blob' }).then(res => { const blob = new Blo ...

  4. LG1290 欧几里德的游戏

    https://www.luogu.com.cn/problem/P1290 博弈论游戏,用到mod. 辗转相除法的过程,会构成n种状态. 到达最后一个状态就赢了. 对于一次过程如果div>1那 ...

  5. 《挑战程序设计竞赛》1.6.2-POJ的题目Ants

    #include <stdio.h> #define max(a, b) (((a) > (b)) ? (a) : (b)) #define min(a, b) (((a) < ...

  6. 【Java】抽象类与抽象方法

    文章目录 抽象类与抽象方法 abstract关键字的使用 abstract修饰类:抽象类 abstract修饰方法:抽象方法 abstract使用上的注意点: 抽象类的匿名子类 模板方法设计模式 抽象 ...

  7. 使用.NET 6开发TodoList应用(29)——实现静态字符串本地化功能

    系列导航及源代码 使用.NET 6开发TodoList应用文章索引 需求 在开发一些需要支持多种语言的应用程序时,我们需要根据切换的语言来对应展示一些静态的字符串字段,在本文中我们暂时不去讨论如何结合 ...

  8. Pytorch之Spatial-Shift-Operation的5种实现策略

    Pytorch之Spatial-Shift-Operation的5种实现策略 本文已授权极市平台, 并首发于极市平台公众号. 未经允许不得二次转载. 原始文档(可能会进一步更新): https://w ...

  9. 《剑指offer》面试题06. 从尾到头打印链表

    问题描述 输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回). 示例 1: 输入:head = [1,3,2] 输出:[2,3,1] 限制: 0 <= 链表长度 <= 10 ...

  10. JSON串、JSON对象、Java对象的相互转换

    对象类型转换2: com.alibaba.fastjson.JSONObject时经常会用到它的转换方法,包括Java对象转成JSON串.JSON对象,JSON串转成java对象.JSON对象,JSO ...