Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent a constructor from being called with a temporary

Tags:

c++

c++11

I have a class whose constructor takes some vectors and stores them.

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_;
};

I would like to make the member variables into references, because in production code the values passed to the constructor are lvalues, whose lifetimes are longer than the object of class X.

However, unit tests usually construct Xes with temporaries, like this:

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

If the members of X are to be references, such calls should be prevented. This can be done by writing a constructor that takes rvalue references and making it =delete.

X(std::vector<int> && ints, std::vector<float> && floats, std::vector<std::string> && strings) = delete;

This would prevent the instantiation if all arguments are temporaries. Unfortunately, it lets through calls where at least one argument is an lvalue:

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

because lvalue references are willing to bind to rvalues, so the (const&,const&,const&) constructor can be called.

Do I have to write every combination of lvalue/rvalue ref arguments (all seven of them) and mark all of them as deleted?

like image 430
Bulletmagnet Avatar asked Jan 22 '18 16:01

Bulletmagnet


3 Answers

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&&) {
    // ...
}
like image 90
T.C. Avatar answered Oct 18 '22 13:10

T.C.


How about a function with non-const lvalue ref arguments? Temporaries cannot bind to those so this should be what you are looking for.

Something like 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"} };
}

The only thing you lose is a bit of const correctness., which can be as thin as only the constructor. That is, if you want const at all. Your question does not make that clear at all.


Because the templates in @T.C.'s answer make my eyes bleed, here's my version of those if you were more inclined towards that solution:

#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"} };
}

This uses C++17 fold expressions to expand the parameter pack over the variadic template arguments. In case C++17 is unavailable, you can replace that with:

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;

Which is admittedly a lot less nice.

like image 27
rubenvb Avatar answered Oct 18 '22 15:10

rubenvb


What if you just defer to the library:

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 is already only constructible from lvalues, so we don't need to do all that work ourselves.

like image 44
Barry Avatar answered Oct 18 '22 14:10

Barry