Войти
ПрограммированиеСтатьиОбщее

Магия шаблонов и C++0x

Внимание! Этот документ ещё не опубликован.

Автор:

Если мы решили использовать обобщенное программирование для решения каких либо задач, следует упомянуть пару нюансов использования шаблонов и выведения типов.

Вот такой например:
допустим, мы определяем следующие специализации шаблонного вектора

template<typename type>
struct vector<type, 2>
{
  vector(type const & x, type const & y);
};
template<typename type>
struct vector<type, 3>
{
  vector(type const & x, type const & y, type const & z);
};
template<typename type>
struct vector<type, 4>
{
  vector(type const & x, type const & y, type const & z, type const & w);
};
теперь, чтобы использовать данные типы, нам нужно постоянно указывать шаблонные параметры
vector<float, 3> v = vector<float, 3>(1, 2, 3);
как-то не айс, классическим решением является нечто в таком духе:
        typedef vector<float, 2> vec2;
        typedef vector<float, 3> vec3;
        typedef vector<float, 4> vec4;

        typedef vector<double, 2> dvec2;
        typedef vector<double, 3> dvec3;
        typedef vector<double, 4> dvec4;

        typedef vector<bool, 2> bvec2;
        typedef vector<bool, 3> bvec3;
        typedef vector<bool, 4> bvec4;

        typedef vector<int, 2> ivec2;
        typedef vector<int, 3> ivec3;
        typedef vector<int, 4> ivec4;

        typedef vector<unsigned int, 2> uvec2;
        typedef vector<unsigned int, 3> uvec3;
        typedef vector<unsigned int, 4> uvec4;
так можно сказать выглядит лучше
vec3 v = vec3(1, 2, 3);

Но ведь мы старались уйти от подобного решения и использовать обобщенное программирование, не так ли?
А вернулись к конкретным специализациям каждого типа, данное решение хоть и имеет право на жизнь, но отменяет принцип обобщенности, чтобы ввести новый тип вектора, нам потребуется определить новую серию typedef'ов.
например:

        typedef vector<complex, 2> сvec2;
        typedef vector<complex, 3> сvec3;
        typedef vector<complex, 4> сvec4;
да и вообще сразу видно, что vec2, vec3, vec4 это совершенно разные типы.
тем временем наблюдательный программист смог бы заметить, что тип вектора четко определяется параметрами конструктора и их количеством и предложил бы нечто вроде:
template<typename type>
vector<type, 2> vec(type const & v0, type const & v1) 
{ 
   return vector<type, 2>(v0, v1); 
}
template<typename type>
vector<type, 3> vec(type const & v0, type const & v1, type const & v2) 
{ 
   return vector<type, 2>(v0, v1, v2); 
}
template<typename type>
vector<type, 4> vec(type const & v0, type const & v1, type const & v2, type const & v3) 
{ 
   return vector<type, 2>(v0, v1, v2, v3); 
}
и применим
vector<float, 3> v = vec(1, 2, 3);
не правда ли элегантно? правда, левая часть выглядит до сих пор косячно, для этого в новый стандарт ввели следующую конструкцию
auto v2 = vec(1, 2);
auto v3 = vec(1, 2, 3);
auto v4 = vec(1, 2, 3, 4);
кто с ходу догадается, что здесь не вызов конструктора какого-то универсального типа?
Но и данное решение не лишено недостатков, этот недостаток тянется еще со времени написания нами коструктора, можно поэтапно его проследить на примере одной из специализации вектора:
template<typename type>
struct vector<type, 2>
{
  vector(type const & x, type const & y);
};
косяк в том, что например:
auto v2 = vec(1, 2.0);
приведет к ошибке компиляции, так как компилятор не может вывести шаблонный параметр для функции vec...

Если бы мы явно вызвали конструктор

auto v2 = vector<int,2>(1, 2.0);
то проблем бы не было, компилятор бы применил неявное преобразование ко второму параметру, и все было бы замечательно, но так как мы выводим тип через шаблоную функцию, компилятор, не может принять за нас решение использовать
vector<int,2>(1, 2.0)
по типу первого параметра, или
vector<float,2>(1, 2.0)
по типу второго параметра.

Если бы я искал путь решения, я бы сначала подумал о таком решении

template<typename type>
struct vector<type, 2>
{
  template<typename other_type>
  vector(other_type const & x, other_type const & y);
};
данная конструкция бы позволила бы нам избежать, неявного преобразования типа в выражении вида
auto v2 = vector<int,2>(1.0, 2.0);
мы бы вызвали явное преобразование к типу компонента вектора прямо в нутри конструктора, и возможность компиляции определялась бы только наличем данного явного преобразования, но к сожалению данная конструкция ни чем не поможет в нашей проблеме.
Правильное решение начинается с объявления следующего конструктора:
template<typename type>
struct vector<type, 2>
{
  template<typename type0, typename type1>
  vector(type0 const & x, type1 const & y);
};
и вызовом явного приведения к типу компонента вектора внутри конструктора, такое объявление конструктора поможет проглотить без нареканий, следующее выражение
auto v2 = vector<int,2>(1, 2.0);
теперь наш конструктор, совсем не зависит от типов передаваемых ему параметров, зависит лишь от возможности явного преобразования из типа параметра конструктора в тип компонента вектора.
В дальнейшем нам следует, лишь понять, что тип компонента вектора полностью определяется правилами
арифметического преобразования
например:
auto v2 = vec(1, 2.0f);
v2 имеет тип vector<float,2>
auto v2 = vec(1.0f, 2.0);
v2 имеет тип vector<double,2>
и тд.
Нам остался, лишь один маленький шажок объяснить все это компилятору, для чего в новом стандарте и вводится следующая конструкция
template<typename type0, typename type1>
auto arithm_type(type0 && v0, type1 && v1) 
   -> decltype (std::forward<type0>(v0) + std::forward<type1>(v1)) 
{ 
   return std::forward<type0>(v0) + std::forward<type1>(v1); 
}; 
теперь мы имеем возможность, вывести тип результата функции используя правила арифметического преобразования, но тут мы сталкиваемся с одной проблемой, нам нужно вывести шаблонный тип, тип шаблонного параметра для которого определяется с помощью правил арифметического преобразования.
В настоящий момент в языке не существует конструкций позволяющих это сделать, например
decltype (std::forward<type0>(v0) + std::forward<type1>(v1))
нельзя использовать в качестве шаблонного параметра,
но как можно было догадаться, решение всегда можно найти, нам всего лишь надо понять, что представляет из себя decltype, а она то как раз определяет тип результата выражения, подобно тому, как sizeof определяет размер результата выражения, и соответственно, следует заметить, что точно так же как и при использовании sizeof выражение не выполняется, и ни каких накладных раскодов во время выполнения нет.
теперь, нужно лишь знать две вещи
1) вызов функции является выражением,
2) шаблонные функции способны выводить типы шаблонных параметров, из типов аргументов функции.
т.е. нас спасет следующая функция
template<size_t dimension, typename type>
vector<type, dimension> vec(type const & value) 
{ 
   return vector<type, dimension>(value); 
}
кроме нее нам лишь следует определить конструктор принимающий один аргумент:
template<typename other_type>
explicit vector(other_type const & value) : base_type((value_type)value) { }
при этом следует заметить, что для нашей функции не важно является конструктор шаблонным или нет, шаблонный параметр, не выводится в конструкторе, шаблонный параметр выводится именно во вспомогательной функции и явно указывается, а шаблонный конструктор, нужен лишь для тех целей, которые мы приводили выше, что бы можно было применять явное преобразование в конструкторе, и конструктор не зависел от типа компонента вектора.
Ну все, до победы остался лишь один шаг
decltype (std::forward<type0>(v0) + std::forward<type1>(v1))
выполнит нам арифетическое преобразование, а
decltype (vec<2>(std::forward<type0>(v0) + std::forward<type1>(v1)))
выведет нам тип вектора.

В итоге у нас должно получиться нечто вроде:

        template<typename type0, typename type1>
        auto vec(type0 && v0, type1 && v1) 
            -> decltype(vec<2>(std::forward<type0>(v0) + std::forward<type1>(v1)))
        { 
            typedef decltype(vec<2>(std::forward<type0>(v0) + std::forward<type1>(v1))) vector;
            return vector(std::forward<type0>(v0), std::forward<type1>(v1)); 
        }

И приведем, для порядка, две оставшиеся функции, для других размерностей

        template<typename type0, typename type1, typename type2>
        auto vec(type0 && v0, type1 && v1, type2 && v2) 
            -> decltype(vec<3>(std::forward<type0>(v0) + std::forward<type1>(v1) + std::forward<type2>(v2)))
        { 
            typedef decltype(vec<3>(std::forward<type0>(v0) + std::forward<type1>(v1) + std::forward<type2>(v2))) vector;
            return vector(std::forward<type0>(v0) + std::forward<type1>(v1) + std::forward<type2>(v2)); 
        }
        template<typename type0, typename type1, typename type2, typename type3>
        auto vec(type0 && v0, type1 && v1, type2 && v2, type3 && v3) 
            -> decltype(vec<4>(std::forward<type0>(v0) + std::forward<type1>(v1) + std::forward<type2>(v2) + std::forward<type3>(v3)))
        { 
            typedef decltype(vec<4>(std::forward<type0>(v0) + std::forward<type1>(v1) + std::forward<type2>(v2) + std::forward<type3>(v3))) vector;
            return vector(std::forward<type0>(v0) + std::forward<type1>(v1) + std::forward<type2>(v2) + std::forward<type3>(v3)); 
        }
ну ни ляпота ли?
auto v2 = vec(1, 2.0f);
auto v3 = vec(1, 2.0, 3.0f);
auto v4 = vec(1, 2.0f, 3.0, complex( 4.0 , 5.0 ));
complex :) надо бы определить соответствующим образом, что бы тип complex'а выводился из аргументов функции так же можно применить, все использованые конструкции для определения арифметических операций, в которых должно на самом деле выполняться арифметическое преобразование
        template<typename type0, size_t dimension, typename type1>
        auto operator +(vector<type0, dimension> const & left, type1 && right )
            -> decltype(vec<dimension>(left.at(0) + std::forward<type1>(right)))
        {
            typedef decltype(vec<dimension>(left.at(0) + std::forward<type1>(right))) vector;
            vector result;
            for(auto i = 0; i < dimension; i++) result.at(i) = left.at(i) + std::forward<type1>(right);
            return result;
        }

        template<typename type0, size_t dimension0, typename type1, size_t dimension1>
        auto operator +(vector<type0, dimension0> const & left, vector<type1, dimension1> const & right )
            -> decltype(vec<dimension0 < dimension1 ? dimension1 : dimension0>(left.at(0) + right.at(0)))
        {
            const auto dimension = dimension0 < dimension1 ? dimension1 : dimension0;
            typedef decltype(vec<dimension>(left.at(0) + right.at(0))) vector;
            vector result;
            for(auto i = 0; i < dimension; i++) 
            {
                auto l = i < dimension0 ? left.at(i) : 0;
                auto r = i < dimension1 ? right.at(i) : 0;
                result.at(i) = l + r;
            }
            return result;
        }
        ...
и можно показывать фокусы
auto v2 = vec(1, 2.0f);
auto v3 = v2 + vec(3.0, 4, 5.0f);
auto v4 = v4 + complex(4.0, 5.0);
кто назовет типы переменных v2, v3, v4?

PS: по приведенным примерам, можно сказать лишь одно, реализация complex из STL (заголовочный файл <complex>) не будет работать с нашим вектором, по ряду причин:

--- конструктора complex в виде complex(4.0, 5.0) не существет, правильным является следующий вызов std::complex<double>(4.0, 5.0), комплексный тип, не выводится из аргументов конструктора, и его следует явно указывать, реализовать возможность вызова complex(4.0, 5.0) можно порекомендовать выполнить в качестве домашнего задания )))

--- std::complex нельзя вывести используя правила арифметических преобразований для complex существуют лишь следующие операторы

template<class Type>
   complex<Type> operator+(
      const complex<Type>& _Left,
      const complex<Type>& _Right
   );
template<class Type>
   complex<Type> operator+(
      const complex<Type>& _Left,
      const Type& _Right
   );
template<class Type>
   complex<Type> operator+(
      const Type& _Left,
      const complex<Type>& _Right
   );
template<class Type>
   complex<Type> operator+(
      const complex<Type>& _Left
   );
как вы можете заметить их можно было бы улучшить используя методику описанную в данной статье :), так же в качестве домашнего задания :)

после того как данные условия будут соблюдены, можно будет с легкостью использовать комлексный тип в качестве типа вектора.

Желаю удачи и успехов в нашем общем деле

#auto, #C++0x, #decltype, #template

4 ноября 2010 (Обновление: 31 мая 2011)

Комментарии [10]