Специализация шаблона оператора конверсии
Здесь в значительной степени академическое упражнение в понимании операторов преобразования, шаблонов и специализированных шаблонов. Шаблон оператора преобразования в следующем коде работает для int
, float
и double
, но сбой при использовании с std::string
... своего рода. Я создал специализацию преобразования в std::string
, которая работает при использовании с инициализацией std::string s = a;
, но сбой при использовании с литой 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
}
Вышеприведенный код генерирует ошибку компилятора в g++ и icc, и они жалуются, что никакое пользовательское преобразование не подходит для преобразования экземпляра MyClass
в std::string
в static_cast
(приведения в стиле C ведут себя одинаково).
Если я заменил вышеприведенный код явными, нетературными версиями оператора преобразования, все будет счастливым:
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();
}
};
Что не так с моей специализацией шаблона для std::string
? Почему он работает для инициализации, но не выполняет листинг?
Update:
После некоторого шаблона wizardry by @luc-danton (метапрограммные трюки, которых я раньше не видел), у меня есть следующий код, работающий в g++ 4.4.5 после включения экспериментальных расширений С++ 0x. Помимо ужаса того, что делается здесь, требуя экспериментальных параметров компилятора, достаточно разумно один, чтобы не делать это. Несмотря на это, это, как мы надеемся, является образовательным для других, как и для меня:
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; }
};
Это, по-видимому, вынуждает компилятор выбрать преобразование operator std::string()
для std::string
, которое проходит мимо какой-либо двусмысленности, с которой сталкивался компилятор.
Ответы
Ответ 1
Вы можете воспроизвести проблему, просто используя
std::string t(a);
В сочетании с фактической ошибкой от GCC (error: call of overloaded 'basic_string(MyClass&)' is ambiguous
) у нас есть сильные подсказки относительно того, что может произойти: есть одна предпочтительная последовательность преобразования в случае инициализации копирования (std::string s = a;
), а в случае прямого инициализация (std::string t(a);
и static_cast
) есть по крайней мере две последовательности, где одна из них не может быть предпочтительной над другой.
Посмотрев на все конструкторы явного конструктора std::basic_string
, принимающие один аргумент (единственные, которые будут рассмотрены при прямой инициализации, но не для инициализации копии), мы найдем explicit basic_string(const Allocator& a = Allocator());
, который на самом деле является единственным явным конструктором.
К сожалению, я не могу много сделать за пределами этой диагностики: я не могу придумать трюк, чтобы обнаружить, что operator std::allocator<char>
создан или нет (я пробовал SFINAE и operator std::allocator<char>() = delete;
, не имел успеха), и я тоже знаю немного о специализациях шаблонов функций, разрешении перегрузки и требованиях к библиотеке, чтобы знать, соответствует ли поведение GCC или нет.
Поскольку вы говорите, что упражнение является академическим, я пощажу вас обычной диатрибой, поскольку неявные операторы преобразования не являются хорошей идеей. Я думаю, что ваш код - хороший пример того, почему так или иначе:)
Я получил SFINAE для работы. Если оператор объявлен как:
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();
Тогда нет никакой двусмысленности, и код будет скомпилирован, специализация для std::string
будет выбрана, и результирующая программа будет вести себя по желанию. У меня все еще нет объяснений, почему инициализация копирования в порядке.
Ответ 2
static_cast
здесь эквивалентно выполнению std::string(a)
.
Обратите внимание, что std::string s = std::string(a);
тоже не скомпилируется. Я предполагаю, что существует множество перегрузок для конструктора, и версия шаблона может преобразовать a
во многие подходящие типы.
С другой стороны, с фиксированным списком преобразований только один из них соответствует точно типу, который принимает конструктор строк.
Чтобы проверить это, добавьте преобразование в const char*
- не templated версия должна начать сбой в том же месте.
(Теперь возникает вопрос, почему работает std::string s = a;
. Тонкие различия между этим и std::string s = std::string(a);
известны только богам.)