5

I have a class which takes a size as a template parameter (live demo):

template <std::size_t SIZE> class A
{
    char b[SIZE];
}

It have multiple constructors for different purposes:

using const_buffer_t = const char (&)[SIZE];
using my_type = A<SIZE>;

A()                         : b{} {} // (1) no params
A(const_buffer_t)           : b{} {} // (2) copy contents of given buffer
A(const char * const)       : b{} {} // (3) copy as many items as they fit into the size
explicit A(const my_type &) : b{} {} // (4) copy constructor

// (5) copy as many items as they fit into the size
template <std::size_t OTHER_SIZE>
A(const char (&)[OTHER_SIZE]) : b{} {}

// (6) copy constructor from another sized A
// copy as many items as they fit into the size
template <std::size_t OTHER_SIZE>
explicit A(const A<OTHER_SIZE> &) : b{} {}

With this set of constructors there's no problem with this instructions:

// CASE 1
// Calls constructor 3: A<5>(const char * const)
// Expecting constructor 5: A<5>(const char (&)[11])
A<5> a("0123456789");

// CASE 2
// As expected, calls constructor 1: A<5>()
A<5> b();

// CASE 3
// As expected, calls constructor 4: A<5>(const A<5> &)
A<5> c(b);

// CASE 4
// As expected, calls constructor 6: A<5>(const A<9> &)
A<9> c(b);

But when calling A<5>("five") there's an ambiguous call between the constructors 2, 3, 4 and 5.

So my questions are:

  • Why the constructor 3 is preferred over the constructor 5 in the CASE 1?
  • Is there a way to disambiguate the constructors 2, 3, 4, 5 when the object A<SIZE> is constructed with a static array of the same size of the template parameter?

Thanks for your attention.

PaperBirdMaster
  • 12,806
  • 9
  • 48
  • 94
  • 3
    (3) is preferred over (5) because (5) is a template. – Xeo Apr 25 '14 at 11:05
  • STL's video has a good explanation of the name lookup rules:http://channel9.msdn.com/Series/C9-Lectures-Stephan-T-Lavavej-Core-C-/Stephan-T-Lavavej-Core-C-1-of-n – EdChum Apr 25 '14 at 11:11
  • Due to overload resolution: http://en.cppreference.com/w/cpp/language/overload_resolution – 101010 Apr 25 '14 at 11:48
  • 2
    (2) looks like a simple function declaration because of [most vexing parse](http://en.wikipedia.org/wiki/Most_vexing_parse). – Constructor Apr 25 '14 at 11:48

2 Answers2

4

Array-to-pointer conversion is considered to be an exact match when ranking conversion sequences during overload resolution (C++11 13.3.3.1.1/1 Table 12). Contrary to your intuition, that means that (3) and (5) are equally good matches for A<5> a("0123456789");. The tie is broken - as Xeo says in his comment - in favor of the non-template (3). You may think to trick the compiler by turning (3) into a template as well:

template <typename=void>
A(const char * const) : b{} {}

but doing so will only result in ambiguity of the construction. There's really no easy way to disambiguate const char (&)[] and const char* overloads: the best solution may be to change (3) to accept a pointer and length:

A(const char * const, std::size_t) : b{} {
  std::cout << "size: " << SIZE << " ctor 3\n";
}

Just in passing, I'll note that adding a size_t argument to the const char* const constructor also disambiguates the A("five") case.

EDIT: There is, however, one reasonable way to disambiguate the char* constructor from the array constructor, accept pointer arguments by reference:

template <typename T,
  typename=typename std::enable_if<
    std::is_same<typename std::remove_cv<T>::type, char>{}
  >::type>
A(T* const&) : b{} { std::cout << "size: " << SIZE << " ctor 3\n"; }

[Credit for this particular trick goes to dyp, and possibly Johannes Schaub or Yakk or me (I'm pretty sure it wasn't me).]

This template effectively latches onto the actual type by reference - before array-to-pointer conversion can occur - and then constrains away references to non-pointer types.

Community
  • 1
  • 1
Casey
  • 41,449
  • 7
  • 95
  • 125
  • 2
    I don't quite remember who suggested this (Johannes Schaub? Yakk? You?), but you can disambiguate via a subtle rule in template type deduction plus SFINAE: http://coliru.stacked-crooked.com/a/17799d4281da2ac9 The array-to-pointer conversion is only applied if the parameter is not a reference. – dyp Apr 25 '14 at 18:44
  • @dyp That is very sneaky - I strongly suspect it was not my idea. It could use some generalizing to work with rvalues...and non-`const` `*`, `* const`, `const * const`. There, general array-to-pointer conversion inhibitor. I think. – Casey Apr 25 '14 at 20:19
  • :) Let's clean up the comments. Sadly I can't find where I've learned that trick, IIRC it was a very similar question. – dyp Apr 25 '14 at 21:30
2
  • Why the constructor 3 is preferred over the constructor 5 in the CASE 1?

    Answer: Due to overload resolution. Non templated class functions are first class citizens and as such have higher overload resolution ranking than templated functions. Thus, constructor 3 is preferred over template constructor 5.

101010
  • 41,839
  • 11
  • 94
  • 168