简体   繁体   中英

Conditionally enable a constructor whenever a member variable can be constructed by variadic arguments

I have a class foo with template parameter Tuple and I want to provide a variadic arguments constructor to initialize a member variable m_elements of type Tuple , whenever the expression m_elements{ static_cast<typename Tuple::value_type>(std::forward<Elements>(elements))... } is defined.

We can do this in the following way:

template<class Tuple>
struct foo
{
    using value_type = typename Tuple::value_type;

    template<class... Elements, class U = Tuple,
        class = decltype(U{ static_cast<value_type>(std::declval<Elements>())... })>
    foo(Elements&&... elements)
        : m_elements{ static_cast<value_type>(std::forward<Elements>(elements))... }
    {}

    Tuple m_elements;
};

Now, whether this constructor is enabled or not should depend on some other conditions too. So, I would need to write something like

template<class... Elements, class U = Tuple,
    class = std::enable_if_t</* some other conditions depending on U */>,
    class = decltype(U{ static_cast<value_type>(std::declval<Elements>())... })>

I would like to check my first condition in terms of std::is_constructible such that I could move this check into the enable_if . Is this possible? I've tried to use std::is_constructible_v<U, decltype(static_cast<value_type>(std::declval<Elements>()))...> , but this doesn't seem to be equivalent to the previous check.

For example, foo<bar<3>>{1, 2, 3}; with

template<std::size_t N>
struct bar
{
    using value_type = double;
    double data[N];
};

will compile with the previous check, but yields an error with the new one.

As Rostislav mentioned, if T is not a function type, std::is_constructible_v<T, Args> is true iff the variable definition T obj(std::declval<Args>()...); is well-formed. That's not the case in bar<1> obj(0.); , cause bar<1> has no corresponding constructor.

In contrast, bar1<1> obj{ 0. }; is well-formed. Using the proposed Detection Toolkit , we could use

template<class T, typename... Arguments>
using initializable_t = decltype(T{ std::declval<Arguments>()... });

template<class T, typename... Arguments>
constexpr bool is_initializable_v = is_detected_v<initializable_t, T, Arguments...>;

and change the check to

template<class... Elements, class U = Tuple,
    class = std::enable_if_t<is_initializable_v<U, decltype(static_cast<value_type>(std::declval<Elements>()))...>>>

I think that's more readable than the plain decltype approach.

The reason that the is_constructible trait behaves differently is that there's indeed no constructor of bar taking the three int values as an argument. In this particular case, aggregate initialization is used.

But you could put the test into an enable_if just by using a simple helper struct :

#include <type_traits>

template<typename T>
struct test : std::true_type
{
};

template<class Tuple>
struct foo
{
    using value_type = typename Tuple::value_type;

    template<class... Elements, class U = Tuple,
        class = std::enable_if_t<
            test<decltype(U{ static_cast<value_type>(std::declval<Elements>())... })>::value>
            /* && your other tests */
            >
    foo(Elements&&... elements)
        : m_elements{ static_cast<value_type>(std::forward<Elements>(elements))... }
    {}

    Tuple m_elements;
};


template<std::size_t N>
struct bar
{
    using value_type = double;
    double data[N];
};

int main()
{
    foo<bar<3>>{1, 2, 3};
}

LIVE

Alternatively, you could push the decltype magic to a separate place by creating a type trait using any of the answers to this question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM