Question

Given this code:

#include <iostream>

template<typename... Args> class X;

template<typename T>
class X<T> {
 public:
    T value;
    X(T value_) : value(value_) {}
};

template<typename T, typename... Args>
class X<T, Args...> : public X<Args...> {
 public:
    T value;
    X(T value_, Args... args) : value(value_), X<Args...>(args...) {}
};

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {
    stream << value_.value;
    return stream;
}

template<typename T, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, Args...>& value_) {
    stream << value_.value << " " << static_cast<X<Args...> const& >(value_);
    return stream;
}

class Person : public X<std::string, int>{
 public:
    Person(std::string name, int age) : X<std::string, int>(name, age) {}
};


int main()
{
   std::cout << Person("Me", 35) << std::endl;
   return 0;
}

Compiled with gcc 4.7.2 using 'g++ -std=c++11 main.cpp', gives the following output when executed:

35

I was expecting it to print:

Me 35

because it should have first matched the operator<< for the direct super-class of Person rather than its super-super-class, right? but that doesn't seems to have happened. It has printed only the int which is the behavior of X<int> and not both items which is the behavior of X<std::string, int>, the direct superclass. Is this expected?

Was it helpful?

Solution 2

There are two problems with the code as posted. Firstly the ambiguity between:

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {

and:

template<typename T, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, Args...>& value_) {

in the case of an empty parameter pack. This can be resolved moving to these two functions instead:

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {

and:

template<typename T, typename T2, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, T2, Args...>& value_) {

Now the template matching ambiguity is resolved.

This leaves us with the second problem. This line:

  std::cout << Person("Me", 35) << std::endl;

Person provieds no implementation of operator<< so the compiler is left to convert to a type that has an implementation, in this case both X< std::string, int > and X< int > have such. However, the compiler has now way to pick between these two as the are both super classes of Person.

This can be resolved in two ways, firstly with the addition of a specific operator<< for Person:

std::ostream& operator <<(std::ostream& stream, const Person &value_) {
   stream << static_cast<const X<std::string, int, int>&>(value_);
   return stream;
}

which works fine but I find to be overly verbose. Alternatively, we can remove the ambiguity by removing the class hierarchy and just using a member variable to contain the 'other' vaules:

template<typename T>
class X<T> {
public:
    T value;
    X(T value_) : value(value_) {}
};

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {
   stream << value_.value << ".";
   return stream;
}


// Empty arg packs will match ambiguously with nothing so make sure the empty pack case is distanct from the base case

template<typename T, typename T2, typename... Args>
class X<T, T2, Args...> {
public:
   T value;
   X<T2, Args...> superValue;
   X(T value_, T2 arg, Args... args) : value(value_), superValue(arg, args...) {}
};

template<typename T, typename T2, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, T2, Args...>& value_) {
   stream << value_.value << "," << value_.superValue;
   return stream;
}

Now Person can only be implicitly cast to X < std::string,int > and nothing else and there is no amibguity. Recent g++'s and clang's are both compile this without issue.

As to whether nothing should match an empty arg pack in template resolution or whether a more direct base class should match in favour of a less direct one in conversion resolution, I leave to people more versed in the c++ standard than myself.

OTHER TIPS

To fix your problem:

#include <iostream>

template<typename... Args> class X;

template<typename T>
class X<T> {
 public:
    T value;
    explicit X(T value_) : value(value_) {}
};

template<typename T, typename... Args>
class X<T, Args...> : public X<Args...> {
 public:
    T value;
    explicit X(T value_, Args... args) : X<Args...>(args...), value(value_) {}
};

This type lets us use tag dispatching to select an overload based on the number of arguments to X:

template<std::size_t> struct compile_time_size {};

Here are 3 overloads. The first errors on 0 arguments, the second handles 1, and the third handles N:

template<typename... Ts>
std::ostream& output_helper(std::ostream&, const X<Ts...>&, compile_time_size<0>) = delete;

template<typename T, typename... Ts>
std::ostream& output_helper(std::ostream& stream, const X<T, Ts...>& value, compile_time_size<1>)
{
    stream << value.value;
    return stream;
}
template<typename T, typename... Ts, std::size_t N>
std::ostream& output_helper(std::ostream& stream, const X<T, Ts...>& value, compile_time_size<N>)
{
    stream << value.value << " " << static_cast< X<Ts...> const& >(value);
    return stream;
}

Our << is now just one overload, which makes reasoning about it easy. It then dispatches to the above helper functions, passing the length explicitly. They redispatch back down to << using ADL to find the function declared after themselves:

template<typename... Ts>
std::ostream& operator<<(std::ostream& stream, const X<Ts...>& value) {
    return output_helper( stream, value, compile_time_size<sizeof...(Ts)>() );
}

live example

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top