4

Having this code:

struct Vec3 {
    int x;
    int y;
    int z;
};

template <typename T>
class myProperty {
public:
   myProperty(const T& initValue) : m_value{initValue} {}
private:
    T m_value;
};

When creating myProperty type object:

myProperty<int> ip{1};
myProperty<Vec3> vp1{{1, 2, 3}};
// myProperty<Vec3> vp2{1, 2, 3}; ERROR: myProperty doesn't have a matching constructor.

Is there an elegant way of making vp2 initialization work? Specializing myProperty for Vec3 is an overkill.

nVxx
  • 661
  • 7
  • 20
  • 1
    Is making `myProperty` itself an aggregate an option? (I.e. remove the constructor and make `m_value` public.) Otherwise the only solution is see is a templated constructor, that forwards its arguments to the `T` constructor: `template myProperty(P &&... p) : m_value(std::forward

    (p)...) {}`.

    – HolyBlackCat Nov 10 '18 at 11:01
  • @HolyBlackCat, that [won't compile](https://onlinegdb.com/HyLLbHV6X). To make it work, you'll have to put extra parentheses, like `m_value({std::forward

    (p)...})`, that way `vp2` case will compile but `vp1` not. And making `myProperty` an agregate is not an option.

    – nVxx Nov 10 '18 at 11:35
  • Oops. It should be `: m_value{std::forward

    (p)...}`, otherwise `myProperty ip{1};` doesn't compile. *"vp2 case will compile but vp1 not"* Does it mean you want both `vp1` and `vp2` to compile, not only `vp2`?

    – HolyBlackCat Nov 10 '18 at 11:38
  • @HolyBlackCat, actually no, I don't want vp1 to compile :) My bad, didn't test your suggestion thoroughly, assumed something like `vp4{myVec3Objet}` won't compile also, but it does, so this looks good :) Would accept the answer if you post it. – nVxx Nov 10 '18 at 11:52
  • 1
    Turns out it breaks copy-construction (because it's a better match than `myProperty(const myProperty &)` if the parameter is non-const). I'll post an answer if I figure out how to fix that in a neat way. – HolyBlackCat Nov 10 '18 at 12:07

1 Answers1

6

A simple solution is to use a variadic template constructor:

template <typename ...P> myProperty(P &&... p) : m_value{std::forward<P>(p)...} {}

It makes myProperty<Vec3> vp2{1, 2, 3}; compile.

Also it stops myProperty<Vec3> vp1{{1, 2, 3}}; from compiling (which seems to match your intentions).

The problem with this option is that it prevents copy construction from working propertly.
(If the parameter is a non-const myProperty<T> lvalue, then this variadic constructor is a better match than myProperty(const myProperty &).)

This can be solved with SFINAE:

C++17 with <experimental/type_traits>:

#include <experimental/type_traits>
#include <utility>

template <typename T, typename ...P> using list_constructible = decltype(T{std::declval<P>()...});

// ...

template
<
    typename ...P,
    typename = std::enable_if_t<std::experimental::is_detected_v<list_constructible, T, P...>>
>
myProperty(P &&... p) : m_value{std::forward<P>(p)...} {}

C++14:

#include <type_traits>
#include <utility>

template <typename...> using void_t = void;
template <typename DummyVoid, template <typename...> class A, typename ...B> struct is_detected : std::false_type {};
template <template <typename...> class A, typename ...B> struct is_detected<void_t<A<B...>>, A, B...> : std::true_type {};
template <typename T, typename ...P> using list_constructible = decltype(T{std::declval<P>()...});

// ...

template
<
    typename ...P,
    typename = std::enable_if_t<is_detected<void, list_constructible, T, P...>::value>
>
myProperty(P &&... p) : m_value{std::forward<P>(p)...} {}
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Yet only an upvote, to have some time to experiment with this solution and also keep the question open for alternative solutions. – nVxx Nov 12 '18 at 12:15