显式删除永不使用的拷贝构造函数给出编译错误

时间:2016-06-02 00:54:34

标签: c++ constructor copy-constructor perfect-forwarding

我正在实现一个SizeTag方法,该方法将获取一个大小值并保留l值引用。

事情很好,在这段代码中,目的是使用T&&构造函数。

但是,如果我明确删除了复制构造函数,编译器将给出错误:

#include <cstdint>
#include <type_traits>
#include <utility>

template <typename T = std::uint64_t>
class SizeTag {
  public: 
    using size_type = std::uint64_t;
    using Type = std::conditional_t<std::is_lvalue_reference<T>::value, const size_type&, size_type>;
    inline const Type& get() const { return _size; }

    SizeTag(T&& sz) : _size(std::forward<T>(sz)) { }
    SizeTag& operator = (const SizeTag&) = delete;

    SizeTag(const SizeTag&) = delete;   // No error if this line removed

  private:
    Type _size;
};

template <typename T>
SizeTag<T> make_size_tag(T&& t) {
  return std::forward<T>(t);
}

int main()
{
    int a = 9;
    make_size_tag(a);
}

为什么会这样?在这种情况下,永远不应该调用复制构造函数。

3 个答案:

答案 0 :(得分:2)

返回classstruct的函数复制返回的对象,作为返回对象的过程的一部分。毕竟,返回的对象必须在某处复制。

虽然允许编译器在编译器向自己证明这是安全的时候忽略编译器或优化副本,但技术上仍然会发生复制。

make_size_tag()返回一个对象。使用T的构造函数隐式完成从SizeTag<T>SizeTag的转换,然后在返回时复制构造的对象。由于复制构造函数为delete d,因此会报告错误。

答案 1 :(得分:2)

函数make_size_tag按值返回SizeTag<T>

让我们回顾function returning by value的工作原理:

  • 有一个临时对象,通常称为返回值对象
  • 对于案例return expression;
    • 表达式复制初始化返回值对象。
    • 这是一个复制省略。
  • 对于案例return { zero_or_more_items };
    • 支撑列表 copy-list-initializes 返回值对象。
  • 如果调用代码使用函数调用初始化变量,则返回值对象是初始值设定项。 (初始化的确切形式可能因调用代码而异)。对于对象的初始化,这也是一个复制省略。

在您的代码中,make_size_tag(a)会将Tmake_size_tag的参数)推断为int&,因为这是一个完美的转发方案。

展开std::forward之后make_size_tag对此T的实例化似乎是:

SizeTag<int&> make_size_tag(int& t)
{
    return t;
}

因为static_cast<int&>(t)t相同,因为t已经是int类型的左值。

如前所述,此代码copy-initializes是返回值对象。所以代码现在的行为类似于:

SizeTag<int&> temp_rv = t;

并且由于t不是SizeTag,因此复制初始化的定义与以下内容相同:

SizeTag<int&> temp_rv = SizeTag<int&>(t);

显然会调用复制/移动操作来从temp_rv类型的临时值初始化SizeTag<int&>。虽然复制省略会省略此副本,但必须存在可访问的复制/移动构造函数。

Jarod42建议的解决方案,在返回表达式周围添加括号,因为等效初始化现在是copy-list-initialization

SizeTag<int&> temp_list_rv { t };

使用temp_list_rv构造函数初始化SizeTag<int&>(int&)

NB;您的代码有一个单独的错误:由于Typeconst uint64_t &_size的{​​{1}}初始化会创建一个临时值,当int构造函数完成时会被销毁;所以标签返回一个悬空引用。 clang警告过这个,但g ++没有。

要解决此问题,您需要将SizeTag更改为与Type相同,以便直接绑定到T&,例如:

a

或使using size_type = typename std::remove_reference<T>::type; 不能作为参考。看来后者会破坏你标签的整个目的,所以你可能需要重新考虑一下你的设计。

为避免生成此悬挂引用的可能性,请在conditional_t中将_size更改为const size_type &。然后编译器(假设您不使用MSVC)将指出问题。

答案 2 :(得分:1)

在这种情况下,您必须使用{}来避免复制/移动构造函数:

template <typename T>
SizeTag<T> make_size_tag(T&& t) {
    return { std::forward<T>(t) } ; // Note the extra {}
}

Demo

使用大括号,使用copy-list-initialization而不使用,您可以创建一个复制/移动构造的临时对象(即使RVO适用)。

标准第6.6.3节:

  

带有braced-init-list的return语句初始化要从指定的初始化列表中通过copy-list-initialization(8.5.4)从函数返回的对象或引用。