template<typename A, typename B> class TClass { public: TClass() { } // Overload #1 public: std::string SomeMethod(A a, B b) { std::ostringstream ostr; ostr << a << "-" << b; return ostr.str(); } // Overload #2 public: std::string SomeMethod(B b, A a) { std::ostringstream ostr; ostr << b << "-" << a; return ostr.str(); } };
So that is a template class with SomeMethod overloads. Why would somebody write such a class? Imagine it is an adder class, and the method overloads could used to add with parameters specified in either order. Following is the way one could use the above (based on the adder example):-
int i = 45; double d = 12.3f; TClass<int, float> t1; const std::string idText = t1.SomeMethod(i, d); // This calls Overload #1 const std::string diText = t2.SomeMethod(d, i); // This calls Overload #2
Seems cool until you try this simple thing
TClass<int, int> t2; const std::string idText = t1.SomeMethod(i, i);
The mighty C++ compiler will complain
Error 1 error C2535: 'std::string TClass<A,B>::SomeMethod(A,B)' : member function already defined or declared <YourFileName.cpp>
A well-tuned C++ programmer would have guessed already how to resolve the situation. But we are are going to discuss a few other things related to the problem, besides the solution.
Part 1: Non-member Functions
Let us say TClass was written for hosting SomeMethod overloads. By that I mean to say SomeMethod overloads are self-sufficient and could be made non-member functions (as global methods or methods inside a namespace).
namespace SF { // Overload #1 template<typename A, typename B> std::string SomeMethod(A a, B b) { std::ostringstream ostr; ostr << a << "-->" << b; return ostr.str(); } // Overload #2 template<typename A, typename B> std::string SomeMethod(B b, A a) { std::ostringstream ostr; ostr << b << "-->" << a; return ostr.str(); } }
Hang on. The above code will still result in a compilation error for SomeMethod<T, T>; same type. We are going to now resolve this situation using one of the tough and less-used powers of C++. We are going have the two overloads, make some changes to one overload using SFINAE principle, and allow instantiation\use of SomeMethod using like or different types. That is not a typo; I spelt it right - SFINAE, a.k.a Substitution Failure Is Not An Error.
SFINAE is a principle adopted by the C++ compiler to prevent resulting in a compilation error when preparing the set of candidate overloads for a particular template instantiation. It is widely used for deliberately excluding one or more template overloads based on some condition. The principle is applied only in the case of templates.
In our case, when SF::SomeMethod<AnyT, AnyT> is called, right now both the overloads are successful candidates. That is why the compiler complains about method redefinition. Suppose we wrote the code in a way so as to detect that the two template types passed are same ("the condition"), and we exclude one of the overloads (consider overload #2). This would help us bind to the one method (overload #1). Well, SFINAE is exactly for such purposes. Alright, let us (re)write code...
// Alternatively, you can use EnableIf facility from Boost library template<bool Condition, typename T = void> struct EnableIf { typename T Type; }; template<typename T> struct EnableIf<false, T> { }; // Alternatively, you can use IsSameType facility from Boost or Loki library template<typename A, typename B> struct IsSameType { enum { Result = false }; }; template<typename A> struct IsSameType<A, A> { enum { Result = true }; }; template<typename A, typename B> struct AreDifferentTypes { enum { Result = !(typename IsSameType<A, B>::Result) }; }; namespace SF { // Overload #1 template<typename A, typename B> std::string SomeMethod(A a, B b) { std::ostringstream ostr; ostr << a << "-->" << b; return ostr.str(); } // Overload #2 template<typename A, typename B> typename EnableIf<AreDifferentTypes<A, B>::Result, std::string>::Type // Evaluates to the return type SomeMethod(B b, A a) { std::ostringstream ostr; ostr << b << "-->" << a; return ostr.str(); } }
We have tweaked overload #2 such that while creating the candidate set for overload resolution, it produces an error when the template parameter types substituted are same. AreDifferentTypes<A, B>::Result will evaluate to false if the template parameters are of the same type, as a result of which the specialized version of EnableIf (EnableIf<false, T>) is chosen which does not define the Type member typedef. This is spotted as an error when selecting candidates for overload resolution but ignored (for a good cause). Now the compiler will be able to bind to the only candidate found (Overload #1). So now you can enjoy using SomeMethod:-
void main() { int i = 12; float f = 34.56; const std::string dtText = SF::SomeMethod<int, float>(i, f); const std::string stText = SF::SomeMethod<int, int>(i, i); }
Part 2 - Member Functions
I could feel the SFINAE hangover. Unfortunately, the SFINAE will not work with template member functions. So before we see how to solve our problem in discussion for member functions, let us see why SFINAE will not come for rescue.
For a template method instantiation\call, the C++ compiler first searches for methods (matching the name), which could be a potential candidate for binding to the method call. During this step, the methods are inspected for the signature (and not code). They are rejected if they are detected to produce an error. In the earlier case, AreDifferentTypes returning false thereby making EnableIf devoid of the typedef Type was the error. So this principle is applied to methods (matching the name) with valid signature. In other words, there cannot be an ill-formed method signature from a template instantiation.
In the case of non-member functions (above), the method decorated with EnableIf will always be considered to have a valid method signature. In other words, depending on the template parameters, the method will get an appropriate return type; although it may fail to be a successful candidate for overload resolution (using SFINAE) when the template parameters are of the same type. Irrespective of whether a template method or method inside a template class is used, the compiler will stop with errors if it has an invalid signature.
Aimed with the above information, let us rewrite the TClass as follows and understand why the principle will not work for template member functions.
template<typename A, typename B> class TClass { public: TClass() { } // Overload #1 public: std::string SomeMethod(A a, B b) { std::ostringstream ostr; ostr << a << "-->" << b; return ostr.str(); } // Overload #2 public: typename EnableIf<AreDifferentTypes<A, B>::Result, std::string>::Type SomeMethod(B b, A a) { std::ostringstream ostr; ostr << b << "-->" << a; return ostr.str(); } };
You will see a whole of bunch of errors, of which we should be interested in the following:-
Error 1 error C2039: 'Type' : is not a member of 'EnableIf<Condition,T>' <FileName> <Line> Error 5 error C2556: 'int TClass<A,B>::SomeMethod(B,A)' : overloaded function differs only by return type from 'std::string TClass<A,B>::SomeMethod(A,B)' <FileName> <Line>
A template is almost a dead piece of code, until instantiated. Try writing a template method (with valid signature) that has few favorite quotes from Romeo Juliet (instead of C++ code) as its body. Unless you don't instantiate\call that method, the compiler will be tolerant. When a template is instantiated, that's when the real code to bind to is cooked specifically for the parameter types. And when that is done, any invalid code will be flagged as an error. So when the SomeMethod (overload #2) is cooked for TClass<AnyT, AnyT> instantiation, it results in an invalid code. That is our Error 1 above - Trying to access a member that does not exist. This means that the SomeMethod is being cooked without a return type being specified. In C++ (like in C) if you don't specify a return type, it is assumed to be int. Since there already is an other SomeMethod with the same method signature but just differing in the return type, Error 5 above is raised.
Another category of member functions are static template methods inside a non-template class, which are equivalent to non-members. Let us leave applying the above explanation to this category as an exercise.
Does that mean we don't have a solution? No, not using SFINAE. But by another friend - Template Specialization.
template<typename A, typename B> class TClass { public: TClass() { } // Overload #1 public: std::string SomeMethod(A a, B b) { std::ostringstream ostr; ostr << a << "-" << b; return ostr.str(); } // Overload #2 public: std::string SomeMethod(B b, A a) { std::ostringstream ostr; ostr << b << "-" << a; return ostr.str(); } }; template<typename A> class TClass<A, A> { public: TClass() { } public: std::string SomeMethod(A a, A b) { std::ostringstream ostr; ostr << a << " - " << b; return ostr.str(); } };
So, we have solved the problem!
Comments