我正在实现一个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);
}
为什么会这样?在这种情况下,永远不应该调用复制构造函数。
答案 0 :(得分:2)
返回class
或struct
的函数复制返回的对象,作为返回对象的过程的一部分。毕竟,返回的对象必须在某处复制。
虽然允许编译器在编译器向自己证明这是安全的时候忽略编译器或优化副本,但技术上仍然会发生复制。
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 };
:
在您的代码中,make_size_tag(a)
会将T
(make_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;您的代码有一个单独的错误:由于Type
是const 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 {}
}
使用大括号,使用copy-list-initialization而不使用,您可以创建一个复制/移动构造的临时对象(即使RVO适用)。
标准第6.6.3节:
带有braced-init-list的return语句初始化要从指定的初始化列表中通过copy-list-initialization(8.5.4)从函数返回的对象或引用。