суббота, 18 октября 2014 г.

Грабли шаблонного конструктора

Допустим у нас есть шаблонный класс с шаблонным конструктором копий и функция create:

#include <string>

template <typename T>
class MyClass {
public:
    MyClass() = default;

    MyClass(T t) : value(t) {}

    template <typename U>
    MyClass(const MyClass<U>& other) : value(other.value) {}

    T value;
};

template <typename T>
MyClass<T> create(T v) {
    return MyClass<T>(v);
}
Также имеются две перегруженные функции:
void func(const MyClass<std::string> &) {}
void func(const MyClass<float> &) {}
Если попытаться скомпилировать такой код:
int main() {
    func(create("Hello"));
    return 0;
}
То получим "странное" сообщение об ошибке:
clang++ -Wall -Wextra -std=c++11 test.cpp -O3 -o test
test.cpp:28:5: error: call to 'func' is ambiguous
    func(create("Hello"));
    ^~~~
test.cpp:16:6: note: candidate function
void func(const MyClass &) {
     ^
test.cpp:19:6: note: candidate function
void func(const MyClass &) {
     ^
1 error generated.
make: *** [all] Error 1
То есть компилятор считает обе функции подходящими вариантами и не знает какую выбрать. На самом деле понятно почему. У MyClass есть шаблонный конструктор копий, который может принимать любой тип. Найдя его при разрешении перегрузки функции func, компилятор даже не проверяет, можно ли реально вызвать этот конструктор, и не будет ли ошибок. А ошибки будут, т.к. const char* не приводится к float.
Такая же проблема была с std::pair до принятие стандарта C++11. То есть такой код:
#include <utility>

void func(const pair<int, int>&) {}
void func(const pair<std::string, std::string>&) {}

int main(int argc, char* argv[]) {
    func(std::make_pair("Hello", "world"));

    return 0;
}
Приводил к такой же ошибке.
Решить эту проблему можно с помощью SFINAE и enable_if:
template <typename T>
class MyClass {
public:
    MyClass() = default;

    MyClass(T t) : value(t) {}

    template <typename U, typename = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    MyClass(const MyClass<U>& other) : value(other.value) {}

    T value;
};
Теперь шаблонный конструктор копий участвует в перегрузке только в том случае, если тип T класса, который передаётся в конструктор конвертируется в тип T класса, который конструируется.
Если шаблонному классу std::enable_if в качестве первого параметра передали true, то внутри него определён typedef с типом второго парамтера. Если же первый параметр false, то typedef не определён.
Поле value в шаблонном классе std::is_convertible равно true или false в зависимости от того, конвертируется ли первый параметр шаблона во второй.
Итого, если в конструкции
std::enable_if<std::is_convertible<U, T>::value>::type>
тип U конвертируется в тип T, то std::enable_if::type определён, и функция учавствует в разрешении перегрузки. В противном случае std::enable_if::type не определён, и вся конструкция является некорректной. Такая ситуация называется "substitution failure". По стандарту она не является ошибкой компиляции (is not an error), и компилятор просто выкидывает шаблонную функцию из рассмотрения. Данный приём называется SFINAE - substitution failure is not an error.

Комментариев нет:

Отправить комментарий