Phil Miller
Phil Miller

Reputation: 38118

Call type's tagged constructor if available, default otherwise

I'm trying to build up some code that wants to declare a local variable (say of type test, as shown below). Construction of that local variable should use a constructor that takes a special Tag argument if such a constructor exists, or the default constructor otherwise.

What we've been able to come up with is as follows, where we specialize to construct either a void argument or a Tag argument, but compilers don't like that:

#include <iostream>
using std::cout;

struct Tag { };

template <bool z>
struct helper {
  using type = void;
};
template <>
struct helper<true> {
  using type = Tag;
};

template <bool z>
static typename helper<z>::type get_arg() {
  return typename helper<z>::type();
}

struct test {
  test(void) { cout << "test(void)\n"; }
  test(Tag x) { cout << "test(Tag)\n"; }

  test(const test&) = delete;
  test(test&&) = delete;
};

template <typename T>
void try_construct() {
  // we would be selecting from one of these by template metaprogramming
  T a{typename helper<false>::type()};
  T b{typename helper<true>::type()};

  T c{get_arg<false>()};
  T d{get_arg<true>()};

  // Then do stuff with the suitably-constructed instance of T
  // . . .
}

int main(void) {
  try_construct<test>();

  return 0;
}

Compiler output:

$ g++ -std=c++11 -c foo.cpp 
foo.cpp: In instantiation of 'void try_construct() [with T = test]':
foo.cpp:38:23:   required from here
foo.cpp:30:37: error: no matching function for call to 'test::test(<brace-enclosed initializer list>)'
   T a{typename helper<false>::type()};
                                     ^
foo.cpp:30:37: note: candidates are:
foo.cpp:22:3: note: test::test(Tag)
   test(Tag x) { cout << "test(Tag)\n"; }
   ^
foo.cpp:22:3: note:   no known conversion for argument 1 from 'void' to 'Tag'
foo.cpp:21:3: note: test::test()
   test(void) { cout << "test(void)\n"; }
   ^
foo.cpp:21:3: note:   candidate expects 0 arguments, 1 provided
foo.cpp:33:23: error: no matching function for call to 'test::test(<brace-enclosed initializer list>)'
   T c{get_arg<false>()};
                       ^
foo.cpp:33:23: note: candidates are:
foo.cpp:22:3: note: test::test(Tag)
   test(Tag x) { cout << "test(Tag)\n"; }
   ^
foo.cpp:22:3: note:   no known conversion for argument 1 from 'helper<false>::type {aka void}' to 'Tag'
foo.cpp:21:3: note: test::test()
   test(void) { cout << "test(void)\n"; }
   ^
foo.cpp:21:3: note:   candidate expects 0 arguments, 1 provided

We know how to test on the presence of the constructor, so I've left that our of the example. If that does end up being relevant in a solution taking a different approach, feel free to go that route.

Our ultimate goal is to require one of the default constructor or the Tag constructor, and neither of the copy or move constructors.

Upvotes: 0

Views: 312

Answers (2)

Andrew H. Hunter
Andrew H. Hunter

Reputation: 561

I think something along these lines works:

#include <type_traits>

template <typename T, bool B = std::is_constructible<Tag, T>> struct H;
template <typename T>
struct H<T, false> {
  T t;
  H() : t() {}
};
template <typename T>
struct H<T, true> {
  T t;
  H() : t(Tag) {}
};

try_construct() {
  H<T> h;
  h.t;
}

Upvotes: 1

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

namespace details {
  template<class T>
  T maybe_tag_construct(std::true_type) {
    return T(Tag{});
  }
  template<class T>
  T maybe_tag_construct(std::false_type) {
    return T();
  }
}
template<class T>
T maybe_tag_construct() {
  return details::maybe_tag_construct<T>( std::is_constructible<T, Tag>{} );
}

now auto t =maybe_tag_construct<test>(); constructs test from Tag iff it works.

It also does elided move construction before , and in no move construction occurs.

In order to pass an instance of void around, you need the "regular void" proposal, which is on track for last I checked.

Upvotes: 3

Related Questions