This actually compiles and works, but it's unclear to me why.
#include <iostream>
template <class T>
class LikeA
{
T m_val{};
public:
LikeA() = default;
explicit LikeA(T iv): m_val(std::move(iv)) {}
LikeA(LikeA<T> const &) = default;
LikeA(LikeA<T> &&) noexcept = default;
~LikeA() noexcept = default;
operator T const &() const { return m_val; }
LikeA<T> &operator=(T nv) { m_val = std::move(nv); return *this; }
LikeA<T> &operator=(LikeA<T> const &n) { m_val = n.m_val; return *this; }
LikeA<T> &operator=(LikeA<T> &&n) { m_val = std::move(n.m_val); return *this; }
};
template <class T>
T f (LikeA<T> i)
{
return i;
}
int main()
{
std::cout << f(LikeA{3.1415927}) << '\n'; // No template argument? Not a syntax error?
return 0;
}
I was previously calling f like f(3.1415927) before I let a lint checker talk me into making one of LikeAs constructors explicit. After that, of course, it couldn't implicitly convert the constant to a LikeA. If you just add braces (i.e. f({3.1415927}) the compiler still doesn't know what to select.
In my full code the actual template argument is a lot more verbose, so just for grins I put the template name LikeA in front of the brace initializers, fully expecting a syntax error.
To my surprise, it compiled and ran.
Since this was MSVC, at first I though it was just Microsoft lulling me into a sense of false security. But I tested it against several compilers (gcc, clang, zigcc) in Compiler Explorer, and it works on all of them.
How does C++ select the correct template specialization? On the surface, argument-dependent lookup would seem to be the answer, but notice there are no angle brackets, and the template doesn't have a default argument. I definitely remember this being a syntax error at some point in the past.
(Function template specialization without templated argument doesn't answer this because OP actually specifies the arguments).
The cppreference on function template arguments has a quick aside about omitting <> but this is a class template. The syntax here appears to require the angle brackets all the time.
Since C++17, compiler can automatically deduce the argument type of a template by using class template argument deduction (CTAD). You can skip defining the templates arguments explicitly if the constructor is able to deduce all template parameters.
So you simply write
int main()
{
std::vector v{2, 4, 6, 8}; // same as std::vector<int>
std::list l{1., 3., 5.}; // same as std::list<double>
std::pair p{false, "hello"}; // same as std::pair<bool, const char *>
std::cout << typeid(v).name() << std::endl;
std::cout << typeid(l).name() << std::endl;
std::cout << typeid(p).name() << std::endl;
}
Under MSVC, it produces the following output
class std::vector<int,class std::allocator<int> >
class std::list<double,class std::allocator<double> >
struct std::pair<bool,char const * __ptr64>
Kindly refer CTAD for more details.
Related
I have a template class item which stores objects of various types T. It also attaches attributes to those objects in instantiation/initialization.
One special thing I want to achieve is that whenever item sees a const char *, it deems and stores it as a std::string. This could be done, as follows.
But in type checking, I found an item instantiated from a const char * is still different in type from an item instantiated from a std::string. Please see the last line with comment false, which I want to make true.
#include <iostream>
#include <string>
#include <type_traits>
using namespace std;
template<typename T>
using bar = typename std::conditional<std::is_same<T, const char *>::value,
string, T>::type;
template<typename T>
class item
{
bar<T> thing;
// other attributes ...
public:
item(T t) : thing(t) {}
// other constructors ...
bar<T> what() const
{
return thing;
}
};
int main()
{
auto a = item("const char *"); // class template argument deduction (C++17)
auto b = item(string("string")); // class template argument deduction (C++17)
cout << std::boolalpha;
cout << (typeid(a.what()) == typeid(b.what())) << endl; // true
cout << (typeid(a) == typeid(b)) << endl; // false
}
My question is: is it possible to make any change to the template class item so that an item instantiated from a const char * becomes the same in type with an item instantiated from a std::string?
In other words, can I make any change to the design of the template class item so that typeid(a) == typeid(b) evaluates to true ?
Thank you !
Note: This follows up a previous question on template function. But I think there's something intrinsically different that it deserves a stand-alone question.
Edit: My goal is to change the design of the template class item (e.g. item signatures), not the code in main, which is assumed to be supplied by users. I want to make life easier for the users of item, by not asking them to explicitly supply type T in instantiation. This is meant to be done by C++17 template class argument deduction or some equivalent workarounds.
Update: Thank you all! Special thanks to #xskxzr, whose one-liner exactly solves my question. With user-defined deduction guides for class template argument deduction, I don't even need the bar<T> technique in my previous code. I put updated code below for your comparison.
#include <iostream>
#include <string>
using namespace std;
template<typename T>
class item
{
// UPDATE: no bar<T> needed any more
T thing;
// other attributes ...
public:
item(T t) : thing(t) {}
// other constructors ...
// UPDATE: no bar<T> needed any more
T what() const
{
return thing;
}
};
item(const char *) -> item<std::string>; // UPDATE: user-defined deduction guide !
int main()
{
auto a = item("const char *"); // class template argument deduction (C++17)
auto b = item(string("string")); // class template argument deduction (C++17)
cout << std::boolalpha;
cout << (typeid(a.what()) == typeid(b.what())) << endl; // true
cout << (typeid(a) == typeid(b)) << endl; // UPDATE: now true !
}
You can add a user-defined deduction guide:
item(const char *) -> item<std::string>;
With this deduction guide, a will be deduced to be item<std::string>.
No, you can't directly make the typeid of two templated objects using different template arguements be the same.
But to achieve your end goal you can use a factory like pattern. It could look something like this:
template<typename T, typename R = T>
item<R> make_item(T&& t)
{
return item<T>(std::forward<T>(t));
}
// Specialization for const char *
template<>
item<std::string> make_item(const char *&& str)
{
return item<std::string>(str);
}
The downside with this approach is that you'll need to construct all of your objects with this factory. And if you have a lot of exceptions you'll need to make a specialization for each exception.
This is more a guess than an answer, but I'd say no. Templates are expanded at compile time, so because you are creating an
item<const char*>
and an
item<std::string>
then the code that gets expanded looks something like
class item1
{
bar<const char*> thing;
// other attributes ...
public:
item(const char* t) : thing(t) {}
// other constructors ...
bar<const char*> what() const
{
return thing;
}
};
class item2
{
bar<std::string> thing;
// other attributes ...
public:
item(std::string t) : thing(t) {}
// other constructors ...
bar<std::string> what() const
{
return thing;
}
};
(More or less; they wouldn't actually be called item1 and item2)
How you chose to evaluate these two types is up to you, but to the compiler they are in fact two different types.
Ok, I'd never seen or used std::conditional before so I wasn't sure what that was doing, but after reading up on it and playing around with your code I did get it to "work" by using
bar<T>
as the template type. So instead of
auto a = item<const char*>("const char *");
auto b = item<string>(string("string"));
I did
auto a = item<bar<const char*>>("const char *");
auto b = item<bar<string>>(string("string"));
The thing is you need the template type to be the same in both cases, meaning the type needs to resolve to std::string before the template gets expanded. As long as you use your conditional, you can define any type.
auto c = item<bar<int>>(5);
Not sure that's a good solution (which is why I said "work"), but see my other answer about the class types actually being different.
I have a template where a function is overloaded so it can handle both an std::string parameter and the type of parameter that the template gets instantiated with. This works fine except when the template is being instantiated with std::string, since this results in two member functions with the same prototype. Thus, I have chosen to specialize that function for this particular case. However, it seems like the compiler (g++ 4.8.1 with flag -std=c++0x) never gets to the point where the specialization is actually overriding the primary template and it complains about the ambiguous overload the before it seems to realize that it should use the specialization. Is there a way to get around this?
#include <iostream>
template<class T>
struct A {
std::string foo(std::string s) { return "ptemplate: foo_string"; }
std::string foo(T e) { return "ptemplate: foo_T"; }
};
template<> //Error!
std::string A<std::string>::foo(std::string s) { return "stemplate: foo_string"; }
int main() {
A<int> a; //Ok!
std::cout << a.foo(10) << std::endl;
std::cout << a.foo("10") << std::endl;
//A<std::string> b; //Error!
//std::cout << a.foo("10") << std::endl;
return 0;
}
This results in compile errors, even if I don't instantiate at all with std::string (it seems that the compiler instantiates with std::string as soon as it sees the specialization and that it, before it actually processes the specialization, complains about the ambiguous overload which the specialization, in turn, will "disambiguate").
Compiler output:
p.cpp: In instantiation of 'struct A<std::basic_string<char> >':
p.cpp:10:27: required from here
p.cpp:6:14: error: 'std::string A<T>::foo(T) [with T = std::basic_string<char>; std::string = std::basic_string<char>]' cannot be overloaded
std::string foo(T e) { return "ptemplate: foo_T"; }
^
p.cpp:5:14: error: with 'std::string A<T>::foo(std::string) [with T = std::basic_string<char>; std::string = std::basic_string<char>]'
std::string foo(std::string s) { return "ptemplate: foo_string"; }
^
I would like it to just skip through the implementation of foo() in the primary template and use the specialization without considering the primary template foo(). Could it be done somehow, maybe with non-type template parameters, or do I have to make a fully specialized class template for std::string with all the code duplication it implies (I prefer not to use inheritance here)... Other suggestions?
When you specilize your member function you still get the double ambiguous declaration. Waht you need is to specialize the struct template:
template<>
struct A<std::string> {
std::string foo(std::string s) { return "ptemplate: foo_string"; }
};
If there are many members to the A struct maybe you can refactor:
template<typename T>
struct Afoo
{
std::string foo(T s) { ... }
std::string foo(std::string s) { ... }
};
template<>
struct Afoo<std::string>
{
std::string foo(std::string s) { ... }
};
template<typename T>
struct A : Afoo<T>
{
//a lot of code
};
I'm going to answer this myself since I've been diving deep into this subject today and I think these solutions are nice. All other posts up to this point have been contributive and have had attractive details with potential in other situations. However, I preferred to do it with these things in mind:
Avoid the use of more than one class template
Avoid too complicated specializations as far as possible
Avoid using inheritance and refactor into base and derived classes
Avoid the use of extra wrappers
Please feel free to comment before I accept it as my answer.
Another good and inspiring post on the subject focusing on the use of member function overloading rather than specializations can be found at explicit specialization of template class member function
Solution 1
template<class T>
struct A {
template<class V = T> std::string foo(T) { return "foo_T"; }
std::string foo(std::string) { return "foo_std::string"; }
std::string foo(const char *) { return "foo_const char *"; }
};
template<> template<>
std::string A<std::string>::foo(std::string s) { return foo(s); }
I think this is a dense and understandable solution allowing all class instantiations to use foo(std::string) and foo(const char *) (for passing a string as an rvalue). The use of a dummy template parameter effectively stops class instantiations with std::string from resulting in ambiguous overloads at the same time as the actual template argument hinders uncontrolled function instantiations with unpredictable function arguments. The only problem might come from a class instantiation with std::string that might use the template instead of the regular member function if explicitly called with foo<std::string>(std::string) in which way I would want the class to use the regular foo(std::string) instead of the function template for other instantiations. This is resolved by using a single template specialization.
Solution 2
template<class T>
struct A {
template<class V> std::string foo(V s) { return foo_private(s); }
private:
template<class V = T> std::string foo_private(T) { return "foo_T"; }
std::string foo_private(const char *) { return "foo_const char *"; }
std::string foo_private(std::string) { return "foo_std::string"; }
};
This version allows us to skip the specialization to the benefit of a second template in the class declaration.
Both versions used with:
int main() {
A<int> a;
std::cout << a.foo(10) << std::endl;
std::cout << a.foo("10") << std::endl;
A<std::string> b;
std::cout << b.foo<std::string>("10") << std::endl;
std::cout << b.foo("10") << std::endl;
return 0;
}
... outputted:
foo_T
foo_const char *
foo_const char *
foo_std::string
The error is saying that you ended up creating two method with the same signature.
That is because the struct has been templated with a std::string as parameter.
You should made the function as a templated function, using its own template parameters 'K' not related to the structure template parameter 'T'. Then you can achieve template specialization for the function only.
I admit that the solution I offer below, is a hacky solution indeed, but it does accomplish what you're trying to do and it's kinda funny. Please consider it thoroughly before you use this ;-)
I work around the issue by creating a new type, called FakeType, which can be constructed from your template-type T. The second overload of foo is now for FakeType<T> instead of T, so even when T == string there will be two different overloads:
template <typename T>
struct FakeType
{
T t;
FakeType(T const &t_): t(t_) {}
operator T() { return t; }
};
template <typename T>
struct A
{
string foo(string s) { return "ptemplate: foo_string"; }
string foo(FakeType<T> e) { return "ptemplate: foo_T"; }
};
For the case that T != string:
A<int>().foo("string"); // will call foo(string s)
A<int>().foo(1); // will call foo(FakeType<int> e)
In the latter case, the int will be promoted to a FakeType<int>, which can be used as a regular int through the conversion operator.
For the case that T == string:
A<string>().foo("string"); // will still call foo(string s)
Because the compiler will always prefer an overload for which no promotion is necessary.
PS. This approach assumes that foo is going to get its arguments either by value, or by const-reference. It will break as soon as you try to pass by reference (this can be fixed).
Consider the following overloaded functions:
template <class T>
void foo(const T& v)
{
std::cout << "Generic version" << std::endl;
}
void foo(std::pair<const void*, std::size_t> p)
{
std::cout << "Pair version" << std::endl;
}
Below, I expect the second overload (the one that takes an std::pair) to be called:
int main()
{
const void* buf = 0;
std::size_t sz = 0;
foo(std::make_pair(buf, sz));
}
However, this code in fact calls the generic version. Why doesn't it bind to the overload that specifically takes an std::pair? Is this a compiler bug? I'm using a pretty old compiler, GCC 4.1.2
You need to declare your specialized function as a template
Your specialized argument type must follow the template parameter (i.e. be a const reference) as well.
Try
template <>
void foo(const std::pair<const void*, std::size_t>& p)
{
...
}
How do I avoid implicit casting on non-constructing functions?
I have a function that takes an integer as a parameter,
but that function will also take characters, bools, and longs.
I believe it does this by implicitly casting them.
How can I avoid this so that the function only accepts parameters of a matching type, and will refuse to compile otherwise?
There is a keyword "explicit" but it does not work on non-constructing functions. :\
what do I do?
The following program compiles, although I'd like it not to:
#include <cstdlib>
//the function signature requires an int
void function(int i);
int main(){
int i{5};
function(i); //<- this is acceptable
char c{'a'};
function(c); //<- I would NOT like this to compile
return EXIT_SUCCESS;
}
void function(int i){return;}
*please be sure to point out any misuse of terminology and assumptions
Define function template which matches all other types:
void function(int); // this will be selected for int only
template <class T>
void function(T) = delete; // C++11
This is because non-template functions with direct matching are always considered first. Then the function template with direct match are considered - so never function<int> will be used. But for anything else, like char, function<char> will be used - and this gives your compilation errrors:
void function(int) {}
template <class T>
void function(T) = delete; // C++11
int main() {
function(1);
function(char(1)); // line 12
}
ERRORS:
prog.cpp: In function 'int main()':
prog.cpp:4:6: error: deleted function 'void function(T) [with T = char]'
prog.cpp:12:20: error: used here
This is C++03 way:
// because this ugly code will give you compilation error for all other types
class DeleteOverload
{
private:
DeleteOverload(void*);
};
template <class T>
void function(T a, DeleteOverload = 0);
void function(int a)
{}
You can't directly, because a char automatically gets promoted to int.
You can resort to a trick though: create a function that takes a char as parameter and don't implement it. It will compile, but you'll get a linker error:
void function(int i)
{
}
void function(char i);
//or, in C++11
void function(char i) = delete;
Calling the function with a char parameter will break the build.
See http://ideone.com/2SRdM
Terminology: non-construcing functions? Do you mean a function that is not a constructor?
8 years later (PRE-C++20, see edit):
The most modern solution, if you don't mind template functions -which you may mind-, is to use a templated function with std::enable_if and std::is_same.
Namely:
// Where we want to only take int
template <class T, std::enable_if_t<std::is_same_v<T,int>,bool> = false>
void func(T x) {
}
EDIT (c++20)
I've recently switched to c++20 and I believe that there is a better way. If your team or you don't use c++20, or are not familiar with the new concepts library, do not use this. This is much nicer and the intended method as outlines in the new c++20 standard, and by the writers of the new feature (read a papers written by Bjarne Stroustrup here.
template <class T>
requires std::same_as(T,int)
void func(T x) {
//...
}
Small Edit (different pattern for concepts)
The following is a much better way, because it explains your reason, to have an explicit int. If you are doing this frequently, and would like a good pattern, I would do the following:
template <class T>
concept explicit_int = std::same_as<T,int>;
template <explicit_int T>
void func(T x) {
}
Small edit 2 (the last I promise)
Also a way to accomplish this possibility:
template <class T>
concept explicit_int = std::same_as<T,int>;
void func(explicit_int auto x) {
}
Here's a general solution that causes an error at compile time if function is called with anything but an int
template <typename T>
struct is_int { static const bool value = false; };
template <>
struct is_int<int> { static const bool value = true; };
template <typename T>
void function(T i) {
static_assert(is_int<T>::value, "argument is not int");
return;
}
int main() {
int i = 5;
char c = 'a';
function(i);
//function(c);
return 0;
}
It works by allowing any type for the argument to function but using is_int as a type-level predicate. The generic implementation of is_int has a false value but the explicit specialization for the int type has value true so that the static assert guarantees that the argument has exactly type int otherwise there is a compile error.
Maybe you can use a struct to make the second function private:
#include <cstdlib>
struct NoCast {
static void function(int i);
private:
static void function(char c);
};
int main(){
int i(5);
NoCast::function(i); //<- this is acceptable
char c('a');
NoCast::function(c); //<- Error
return EXIT_SUCCESS;
}
void NoCast::function(int i){return;}
This won't compile:
prog.cpp: In function ‘int main()’:
prog.cpp:7: error: ‘static void NoCast::function(char)’ is private
prog.cpp:16: error: within this context
For C++14 (and I believe C++11), you can disable copy constructors by overloading rvalue-references as well:
Example:
Say you have a base Binding<C> class, where C is either the base Constraint class, or an inherited class. Say you are storing Binding<C> by value in a vector, and you pass a reference to the binding and you wish to ensure that you do not cause an implicit copy.
You may do so by deleting func(Binding<C>&& x) (per PiotrNycz's example) for rvalue-reference specific cases.
Snippet:
template<typename T>
void overload_info(const T& x) {
cout << "overload: " << "const " << name_trait<T>::name() << "&" << endl;
}
template<typename T>
void overload_info(T&& x) {
cout << "overload: " << name_trait<T>::name() << "&&" << endl;
}
template<typename T>
void disable_implicit_copy(T&& x) = delete;
template<typename T>
void disable_implicit_copy(const T& x) {
cout << "[valid] ";
overload_info<T>(x);
}
...
int main() {
Constraint c;
LinearConstraint lc(1);
Binding<Constraint> bc(&c, {});
Binding<LinearConstraint> blc(&lc, {});
CALL(overload_info<Binding<Constraint>>(bc));
CALL(overload_info<Binding<LinearConstraint>>(blc));
CALL(overload_info<Binding<Constraint>>(blc));
CALL(disable_implicit_copy<Binding<Constraint>>(bc));
// // Causes desired error
// CALL(disable_implicit_copy<Binding<Constraint>>(blc));
}
Output:
>>> overload_info(bc)
overload: T&&
>>> overload_info<Binding<Constraint>>(bc)
overload: const Binding<Constraint>&
>>> overload_info<Binding<LinearConstraint>>(blc)
overload: const Binding<LinearConstraint>&
>>> overload_info<Binding<Constraint>>(blc)
implicit copy: Binding<LinearConstraint> -> Binding<Constraint>
overload: Binding<Constraint>&&
>>> disable_implicit_copy<Binding<Constraint>>(bc)
[valid] overload: const Binding<Constraint>&
Error (with clang-3.9 in bazel, when offending line is uncommented):
cpp_quick/prevent_implicit_conversion.cc:116:8: error: call to deleted function 'disable_implicit_copy'
CALL(disable_implicit_copy<Binding<Constraint>>(blc));
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Full Source Code: prevent_implicit_conversion.cc
Well, I was going to answer this with the code below, but even though it works with Visual C++, in the sense of producing the desired compilation error, MinGW g++ 4.7.1 accepts it, and invokes the rvalue reference constructor!
I think it must be a compiler bug, but I could be wrong, so – anyone?
Anyway, here's the code, which may turn out to be a standard-compliant solution (or, it may turn out that that's a thinko on my part!):
#include <iostream>
#include <utility> // std::is_same, std::enable_if
using namespace std;
template< class Type >
struct Boxed
{
Type value;
template< class Arg >
Boxed(
Arg const& v,
typename enable_if< is_same< Type, Arg >::value, Arg >::type* = 0
)
: value( v )
{
wcout << "Generic!" << endl;
}
Boxed( Type&& v ): value( move( v ) )
{
wcout << "Rvalue!" << endl;
}
};
void function( Boxed< int > v ) {}
int main()
{
int i = 5;
function( i ); //<- this is acceptable
char c = 'a';
function( c ); //<- I would NOT like this to compile
}
I first tried PiotrNycz's approach (for C++03, which I'm forced to use for a project), then I tried to find a more general approach and came up with this ForcedType<T> template class.
template <typename T>
struct ForcedType {
ForcedType(T v): m_v(v) {}
operator T&() { return m_v; }
operator const T&() const { return m_v; }
private:
template <typename T2>
ForcedType(T2);
T m_v;
};
template <typename T>
struct ForcedType<const T&> {
ForcedType(const T& v): m_v(v) {}
operator const T&() const { return m_v; }
private:
template <typename T2>
ForcedType(const T2&);
const T& m_v;
};
template <typename T>
struct ForcedType<T&> {
ForcedType(T& v): m_v(v) {}
operator T&() { return m_v; }
operator const T&() const { return m_v; }
private:
template <typename T2>
ForcedType(T2&);
T& m_v;
};
If I'm not mistaken, those three specializations should cover all common use cases. I'm not sure if a specialization for rvalue-reference (on C++11 onwards) is actually needed or the by-value one suffices.
One would use it like this, in case of a function with 3 parameters whose 3rd parameter doesn't allow implicit conversions:
function(ParamType1 param1, ParamType2 param2, ForcedType<ParamType3> param3);
Here's a largely academic exercise in understanding conversion operators, templates and template specializations. The conversion operator template in the following code works for int, float, and double, but fails when used with std::string... sort of. I've created a specialization of the conversion to std::string, which works when used with initialization std::string s = a;, but fails when used with a cast static_cast<std::string>(a).
#include <iostream>
#include <string>
#include <sstream>
class MyClass {
int y;
public:
MyClass(int v) : y(v) {}
template <typename T>
operator T() { return y; };
};
template<>
MyClass::operator std::string() {
std::stringstream ss;
ss << y << " bottles of beer.";
return ss.str();
}
int main () {
MyClass a(99);
int i = a;
float f = a;
double d = a;
std::string s = a;
std::cerr << static_cast<int>(a) << std::endl;
std::cerr << static_cast<float>(a) << std::endl;
std::cerr << static_cast<double>(a) << std::endl;
std::cerr << static_cast<std::string>(a) << std::endl; // Compiler error
}
The above code generates a compiler error in g++ and icc, both complaining that no user-defined conversion is suitable for converting a MyClass instance to a std::string on the static_cast (C-style casts behave the same).
If I replace the above code with explicit, non-template versions of the conversion operator, everything is happy:
class MyClass {
int y;
public:
MyClass(int v) : y(v) {}
operator double() {return y;}
operator float() {return y;}
operator int() {return y;}
operator std::string() {
std::stringstream ss;
ss << y << " bottles of beer.";
return ss.str();
}
};
What is wrong with my template specialization for std::string? Why does it work for initialization but not casting?
Update:
After some template wizardry by #luc-danton (meta-programming tricks I'd never seen before), I have the following code working in g++ 4.4.5 after enabling experimental C++0x extensions. Aside from the horror of what is being done here, requiring experimental compiler options is reason enough alone to not do this. Regardless, this is hopefully as educational for others as it was for me:
class MyClass {
int y;
public:
MyClass(int v) : y(v) {}
operator std::string() { return "nobody"; }
template <
typename T
, typename Decayed = typename std::decay<T>::type
, typename NotUsed = typename std::enable_if<
!std::is_same<const char*, Decayed>::value &&
!std::is_same<std::allocator<char>, Decayed>::value &&
!std::is_same<std::initializer_list<char>, Decayed>::value
>::type
>
operator T() { return y; }
};
This apparently forces the compiler to choose the conversion operator std::string() for std::string, which gets past whatever ambiguity the compiler was encountering.
You can reproduce the problem by just using
std::string t(a);
Combined with the actual error from GCC (error: call of overloaded 'basic_string(MyClass&)' is ambiguous) we have strong clues as to what may be happening: there is one preferred conversion sequence in the case of copy initialization (std::string s = a;), and in the case of direct initialization (std::string t(a); and static_cast) there are at least two sequences where one of them can't be preferred over the other.
Looking at all the std::basic_string explicit constructors taking one argument (the only ones that would be considered during direct initialization but not copy initialization), we find explicit basic_string(const Allocator& a = Allocator()); which is in fact the only explicit constructor.
Unfortunately I can't do much beyond that diagnostic: I can't think of a trick to discover is operator std::allocator<char> is instantiated or not (I tried SFINAE and operator std::allocator<char>() = delete;, to no success), and I know too little about function template specializations, overload resolution and library requirements to know if the behaviour of GCC is conforming or not.
Since you say the exercise is academic, I will spare you the usual diatribe how non-explicit conversion operators are not a good idea. I think your code is a good enough example as to why anyway :)
I got SFINAE to work. If the operator is declared as:
template <
typename T
, typename Decayed = typename std::decay<T>::type
, typename = typename std::enable_if<
!std::is_same<
const char*
, Decayed
>::value
&& !std::is_same<
std::allocator<char>
, Decayed
>::value
&& !std::is_same<
std::initializer_list<char>
, Decayed
>::value
>::type
>
operator T();
Then there is no ambiguity and the code will compile, the specialization for std::string will be picked and the resulting program will behave as desired. I still don't have an explanation as to why copy initialization is fine.
static_cast here is equivalent of doing std::string(a).
Note that std::string s = std::string(a); doesn't compile either. My guess is, there are plenty of overloads for the constructor, and the template version can convert a to many suitable types.
On the other hand, with a fixed list of conversions, only one of those matches exactly a type that the string's constructor accepts.
To test this, add a conversion to const char* - the non-templated version should start failing at the same place.
(Now the question is why std::string s = a; works. Subtle differences between that and std::string s = std::string(a); are only known to gods.)