Метод работы с контейнером: жестко закодировать тип контейнера или использовать общие итераторы шаблонов?

У меня есть код, в котором концептуально мои входные данные представляют собой некоторый контейнер из Foo объектов. Код «обрабатывает» эти объекты один за другим, и желаемый результат — заполнить контейнер FooProduct объектами результатов.

Мне нужен только один проход через входной контейнер. «Обработка» выполняется с сохранением состояния (это не std::transform()), а количество объектов результата не зависит от количества объектов ввода.

Навскидку я увидел здесь два очевидных способа определения API.

Самый простой способ сделать это — жестко запрограммировать конкретный тип контейнера. Например, я мог бы решить, что ожидаю vector параметров, например:

void ProcessContainerOfFoos(const std::vector<Foo>& in, std::vector<FooProduct>&out);

Но на самом деле у меня нет причин ограничивать клиентский код определенным типом контейнера. Вместо того, чтобы ограничивать типы параметров только vector, я мог бы сделать метод универсальным и использовать итераторы в качестве параметров шаблона:

/**
 * @tparam Foo_InputIterator_T An input iterator giving objects of type Foo.
 * @tparam FooProduct_OutputIterator_T An output iterator writing objects 
 *                                     of type FooProduct.
 */
template<typename Foo_InputIterator_T, typename FooProduct_OutputIterator_T >
void ProcessContainerOfFoos(Foo_InputIterator_T first, Foo_InputIterator_T last,
                     FooProduct_OutputIterator_T out);

Я спорю между этими двумя формулировками.

Соображения

Мне первый код кажется "проще", а второй "правильнее":

  • Нешаблонные типы делают подпись более четкой; Мне не нужно объяснять в документации, какие типы использовать и каковы ограничения на параметр шаблона.
  • Без шаблонов я могу скрыть реализацию в файле .cpp; с шаблонами мне нужно будет представить реализацию в заголовочном файле, заставив клиентский код включать все, что мне нужно для фактической логики обработки.
  • Шаблонная версия выглядит так, как будто она более четко выражает мое намерение, потому что я предпочел бы быть безразличным к тому, какой тип контейнера используется.
  • Шаблонная версия является более гибкой и тестируемой — например, в моем коде я могу использовать некоторую пользовательскую структуру данных MySuperEfficientVector , но я все равно смогу протестировать MyFooProcessor без какой-либо зависимости от пользовательского класса.

Помимо субъективного выбора с учетом этих соображений, есть ли основная причина выбрать один из них вместо другого? Точно так же есть ли лучший способ создать этот API, который я упустил? сильный>


person Ziv    schedule 22.01.2015    source источник
comment
Лично я бы использовал версию шаблона, но я бы вернул std::vector<FooProduct> по значению из функции, чтобы воспользоваться преимуществами семантики перемещения.   -  person NathanOliver    schedule 22.01.2015
comment
@NathanOliver: ты имеешь в виду template<typename Foo_InputIterator_T> std::vector<FooProduct> ProcessContainerOfFoos(Foo_InputIterator_T first, Foo_InputIterator_T last); ? Я не понимаю, в чем тут преимущество. Чем перемещение вектора лучше, чем использование выходного итератора?   -  person Ziv    schedule 22.01.2015
comment
Это именно то, что я имею в виду. Причина, по которой я предлагаю это, состоит в том, чтобы сохранить шаги для вызывающей функции. Если вы используете итератор вывода, то пользователю необходимо создать вектор, а затем вызвать вашу функцию и передать итератор функции из вектора. Если вы предоставите возврат вектора, пользователь может объявить вектор и инициализировать его функцией, которая сделает его совместимым с RAII. std::vector<FooProduct> foo = ProcessContainerOfFoos(bar.begin(), bar.end());. Поскольку мы можем использовать семантику перемещения при возврате по значению, производительность при этом не снижается.   -  person NathanOliver    schedule 22.01.2015
comment
@NathanOliver, но затем вы блокируете вывод, чтобы он также был вектором, что раздражает. Лично я бы выбрал вариант полного итератора... вы можете пропустить повторную инициализацию с помощью итератора вставки.   -  person IdeaHat    schedule 22.01.2015
comment
@IdeaHat Если он хочет предложить эту гибкость, я полностью согласен с полным вариантом итератора.   -  person NathanOliver    schedule 22.01.2015
comment
Я бы также вернул выходной вектор вместо передачи неконстантной ссылки. Но только в жестко запрограммированной версии. И я бы вернул итератор вывода после последнего элемента из версии шаблона.   -  person eerorika    schedule 22.01.2015


Ответы (2)


Помимо соображений, которые вы уже перечислили:

  • Версия шаблона позволяет клиентскому коду передавать любой диапазон итераторов, например поддиапазон или обратные итераторы, а не только весь контейнер от начала до конца.
  • Версия шаблона позволяет передавать типы значений, отличные от Foo. Чтобы это было полезно, обработка, конечно, должна быть общей.
  • Если шаблон работает только с определенным типом значения, а пользователь пытается использовать итераторы для неправильного типа, сообщение об ошибке может не очень точно описать его ошибку. Если это вас беспокоит, вы можете дать пользователю лучшую ошибку, используя признаки типа: static_assert(std::is_same<Iter::value_type, Foo>::value, "I want my Foo"); Пока предложение концепций не будет добавлено в стандарт, нет хорошего способа сообщить пользователю требования типа шаблона в подписи.

Существует также возможность предоставления обеих функций. Жестко закодированный можно делегировать шаблонной версии. Это дает вам преимущества обеих версий за счет раздувания вашего API.

person eerorika    schedule 22.01.2015
comment
Очень хорошие моменты. На самом деле меня особенно интересуют случаи, когда я знаю точно, что такое класс Foo, и полагаюсь на него; если я хочу универсальный тип, то, конечно, я хочу дженерики. Что меня интригует, так это то, что делать, когда мне нужен конкретный тип, но общий контейнер. - person Ziv; 22.01.2015
comment
@Ziv ничего не нужно делать с шаблоном, когда вы знаете, какой тип значения ожидать. Если шаблон не может работать с каким-либо другим типом, просто задокументируйте это пользователю. Если пользователь попытается использовать недопустимый тип, сообщение об ошибке может быть не очень описательным. Если это вас беспокоит, вы можете использовать что-то вроде static_assert(std::is_same<Iter::value_type, Foo>::value, "I want my Foo"); для лучшей ошибки. - person eerorika; 22.01.2015

Это зависит. Если эта функция будет использоваться с векторами на данный момент, зачем беспокоиться?

Я предлагаю делать шаблонную версию только тогда, когда это необходимо. Предсказать такие вещи заранее сложно.

person Nick Zavaritsky    schedule 22.01.2015