пятница, 6 июня 2008 г.

Опять шаблоны

Я люблю С++ за то, что код делающий что-то простое может быть сколь угодно простым, но в то-же время сколь угодно сложным. Например нужно написать функцию добавляющую элемент в контейнер, но конкретный тип контейнера неизветсен, вот простой вариант:
#include <iostream>
#include <vector>

template <class C, class T>
void add (T inserter, C value)
{
    *inserter = value;
}


int main(int argc, char *argv[])
{
    std::vector <int> arr;
    add(std::inserter(arr, arr.begin()), 1);
    add(std::inserter(arr, arr.begin()), 2);
    add(std::inserter(arr, arr.begin()), 3);
    for (int i = 0; i != arr.size(); i++)
    {
        std::cout << arr[i] << std::endl;
    }
    system("pause");
}
Функция add получает 2 параметра, первый должен быть insert_iterator-ом, второй - значение которое добавляется в контейнер. Что здесь не так? Тип первого параметра может быть любым, можно например засунуть туда указатель на int и все скомпилируется, но будет неправильно работать. По сигнатуре функции непонятно что она должна получать, указатель на int, или все-же insert_iterator (это не очень понятно и по коду функции (: Если объяснить компилятору более доходчиво, то получится что-то вроде этого:
#include <iostream>
#include <vector>
#include <list>

template < 
    template <class T, class A> class container,
    template <class T> class allocator,
    class Arg
>
void add ( std::insert_iterator< container<Arg, allocator<Arg> > > inserter, Arg value ) 
{
    *inserter = value;
}

int main(int argc, char *argv[])
{
    std::vector <int> arr;
    add(std::inserter(arr, arr.begin()), 1);
    add(std::inserter(arr, arr.begin()), 2);
    add(std::inserter(arr, arr.begin()), 3);
    for (int i = 0; i != arr.size(); i++)
    {
        std::cout << arr[i] << std::endl;
    }
    system("pause");
}
Этот код немного сложнее чем предыдущий. Функция add работает так-же, но имеет теперь три шаблонных параметра а не два, причем первые два - шаблонные(шаблонные параметры) . И теперь указатель на int туда передать нельзя, что очень хорошо. Уверен что можно усложнить этот пример еще и даже получить из этого какую-то пользу, возможности С++ безграничны :)) Update В этом коде есть небольшая ошибка, в функцию add, insert_iterator нужно передавать по ссылке, если передавать по значению, то будут проблемы с вектором. Дело в том, что после перераспределения памяти, итератор, который передается в add не изменится, а изменится только его локальная копия. Это приведет к тому, что итератор, указывающий на начало вектора, который хранится внутри insert_iterator-a станет показывать в неизвестность (пример работает потому, что inserter создается для каждого вызова add заново). Для list и deque это неактуально, так как там insert не приводит к порче итераторов.

3 комментария:

  1. По моему вы пошли по неправильному пути: надо было не защищать функцию от передачи указателя, а наоборот поддержать такое поведение, написав
    *inserter++ = value;

    Тогда в функцию можно будет передавать любой output iterator.

    Вместо этого вы специализировали функцию только для работы с std::inserter. Так в C++ не делается =). А если мне потом придется обернуть этот инсертер, чтобы он округлял значения до тысячи? Если уж было необходимо было защититься от передачи в функцию указателя, это можно было сделать это явно при помощи static-assert или SFINAE.

    Далее. Ваш коментарий по поводу протухания итераторов вектора после переаллокации не лишен здавого смысла, но std::inserter (как и прочие std::*_inserter-ы) должен это обрабатывать (он хранит ссылку на контейнер), т.к. польностью поддерживает интерфейс output iterator, а значит может передаваться по значению.

    ОтветитьУдалить
  2. В общем вы правы, код получился не гибкий. Но, просто этот надуманый пример, должен был продемонстрировать, как можно наложить ограничения на параметр шаблона, вот так..)

    по поводу протухания итераторов, я то-же так думал, но все-же его нельзя передавать по значению..
    опреатор присваивания для insert_iterator-a реализован примерно так:
    iter = container.insert(iter, value);
    iter++;
    return *this;

    тоесть итератор, внутри конкретного insert_iterator-a останется валидным в любом случае.
    Возможно проблема в том, что iter - передается по значению при копировании, поэтому, когда iter изменяется внутри функции, исходный итератор об этом не знает...
    в общем, проще немного переделать мой пример:

    std::insert_iterator iter = std::inserter(arr, arr.begin());
    for(int i = 0; i < 100; ++i)
    {
    add(iter, i);
    }

    и попробовать передавать итератор в add по ссылке и по значению)).

    ОтветитьУдалить
  3. В общем вы правы, код получился не гибкий. Но, просто этот надуманый пример, должен был продемонстрировать, как можно наложить ограничения на параметр шаблона, вот так..)

    по поводу протухания итераторов, я то-же так думал, но все-же его нельзя передавать по значению..
    опреатор присваивания для insert_iterator-a реализован примерно так:
    iter = container.insert(iter, value);
    iter++;
    return *this;

    тоесть итератор, внутри конкретного insert_iterator-a останется валидным в любом случае.
    Возможно проблема в том, что iter - передается по значению при копировании, поэтому, когда iter изменяется внутри функции, исходный итератор об этом не знает...
    в общем, проще немного переделать мой пример:

    std::insert_iterator iter = std::inserter(arr, arr.begin());
    for(int i = 0; i < 100; ++i)
    {
    add(iter, i);
    }

    и попробовать передавать итератор в add по ссылке и по значению)).

    ОтветитьУдалить