如何防止一个构造函数被一个临时对象调用

6

我有一个类,它的构造函数接收一些向量并将它们存储。

struct X {
    X(std::vector<int> const& ints, std::vector<float> const& floats, std::vector<std::string> const& strings)
    : ints_{ints}
    , floats_{floats}
    , strings_{strings}      
    {};

    std::vector<int> ints_;
    std::vector<float> floats_;
    std::vector<std::string> strings_;
};

我希望将成员变量转换为引用类型,因为在生产代码中传递给构造函数的值是左值,其生命周期比类X的对象更长。

然而,单元测试通常使用临时变量来构建X对象,例如:

X x{ {42}, {3.14f}, {"hello"} };

如果X的成员应该是引用,那么应该避免这样的调用。这可以通过编写一个接受右值引用的构造函数并将其设置为=delete来实现。
X(std::vector<int> && ints, std::vector<float> && floats, std::vector<std::string> && strings) = delete;

如果所有参数都是临时变量,则会防止实例化。不幸的是,它允许通过至少有一个参数为左值的调用:

std::vector<std::string> no_strings;
X x{ {42}, {3.14f}, no_strings };

因为左值引用可以绑定到右值,所以可以调用 (const&, const&, const&) 构造函数。
我是否需要编写每种左值/右值引用参数的组合(共七种),并将它们全部标记为已删除?

这个回答讨论了一个类似的问题,但我认为它不适用于引用成员,因为如果我没记错的话,std::move(const T&) 总是会导致复制。 - 0x5453
3个回答

3
template<class T, class U>
using is_lvalue_reference_to_possibly_const = std::integral_constant<bool,
    std::is_same<T, const U&>::value || std::is_same<T, U&>::value>;

template<class VI, class VF, class VS>
using check_value_cat_for_ctor = typename std::enable_if<
    is_lvalue_reference_to_possibly_const<VI, std::vector<int>> &&
    is_lvalue_reference_to_possibly_const<VF, std::vector<float>> &&
    is_lvalue_reference_to_possibly_const<VS, std::vector<string>>>::type;

template<class VI, class VF, class VS, class = check_value_cat_for_ctor<VI, VF, VS>>
X(VI&&, VF&&, VS&&) {
    // ...
}

我宁愿不为此将构造函数变成模板。 - Bulletmagnet

3
如何使用非const左值引用参数的函数?临时变量无法绑定到这些变量,因此这应该是您要寻找的内容。类似于this的东西:
#include <string>
#include <vector>

struct X {
    X(std::vector<int>& ints,
      std::vector<float>& floats,
      std::vector<std::string>& strings)
    : ints_{ints}
    , floats_{floats}
    , strings_{strings}      
    {};

    std::vector<int>& ints_;
    std::vector<float>& floats_;
    std::vector<std::string>& strings_;
};

int main()
{
    X x{ {42}, {3.14f}, {"hello"} };
}

你失去的只是一点 const 的正确性,这可能只存在于构造函数中。也就是说,如果你需要 const。但是,你的问题并没有明确表述。

由于@T.C.的回答中的模板让我眼花缭乱,因此这是我的版本,如果你更倾向于这个解决方案:

#include <string>
#include <type_traits>
#include <vector>

template<typename ... Ts>
constexpr bool are_not_temporaries = (std::is_lvalue_reference<Ts>::value&&...);

struct X
{
  template<typename VI, typename VF, typename VS,
           typename = std::enable_if_t<are_not_temporaries<VI, VF, VS>, void>>
  X(VI&& ints, VF&& floats, VS&& strings)
    : ints_{ints}
    , floats_{floats}
    , strings_{strings}      
    {};

    std::vector<int>& ints_;
    std::vector<float>& floats_;
    std::vector<std::string>& strings_;
};

int main()
{
    X x{ {42}, {3.14f}, {"hello"} };
}

这里使用了C++17的折叠表达式来扩展参数包,覆盖可变模板参数。如果无法使用C++17,您可以将其替换为:

template<typename T1, typename T2, typename T3>
constexpr bool are_not_temporaries = std::is_lvalue_reference<T1>::value 
                                  && std::is_lvalue_reference<T2>::value 
                                  && std::is_lvalue_reference<T3>::value;

诚然,这要差得多。


如果 OP 不想要 const,那么你的 is_lvalue_reference 检查就会无法阻止用户传递一个 converts_to_a_temporary_vector 左值的情况。这就是为什么我选择要求精确匹配的原因。 - T.C.

3
如果你只是推迟使用库,会怎样呢?
template <typename T>
using vector_ref = std::reference_wrapper<std::vector<T> const>;

struct X {
  X(vector_ref<int> ints, vector_ref<float> floats, vector_ref<std::string> strings);
};

std::reference_wrapper 已经只能从左值构造,所以我们不需要自己做那么多工作。


注意:如果成员是 const &,则初始化程序必须像这样写:ints_{ints.get()} 而不是 ints_{ints}。否则编译器会抱怨“将引用成员绑定到临时对象(在构造函数退出之前一直存在)”。 - Bulletmagnet
@Bulletmagnet 嗯?.get()operator T&()的作用是一样的。使用的编译器/版本是什么? - Barry
我也曾认为如此,但是在clang 4.0,clang 6.0,gcc 7.2和gcc 8.0中,如果直接(没有使用.get())将reference_wrapper传递到具有花括号的成员初始化程序时,都会发出警告。 如果成员初始化程序使用圆括号,则不会发生警告。 - Bulletmagnet
列表初始化引用很奇怪,实际上在这种情况下会创建一个临时对象。 (参见https://timsong-cpp.github.io/cppwp/dcl.init.list#3.10) - T.C.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接