+7 (495) 229-0436   shopadmin@itshop.ru 119334, г. Москва, ул. Бардина, д. 4, корп. 3
 
 
Вход
 
 
Каталог
 
 
Подписка на новости
Новости ITShop
Windows 7 и Office: Новости и советы
Обучение и сертификация Microsoft
Вопросы и ответы по MSSQLServer
Delphi - проблемы и решения
Adobe Photoshop: алхимия дизайна
 
Ваш отзыв
Оцените качество магазина ITShop.ru на Яндекс.Маркете. Если вам нравится наш магазин - скажите об этом Google!
 
 
Способы оплаты
 
Курс расчета
 
 1 у.е. = 94.09 руб.
 
 Цены показывать:
 
 
 
 
  
Новости, статьи, акции
 

Как правильно скопировать массив и при чем тут SFINAE

20.09.2012 15:04
kibergus

Копировать элементы из одного контейнера в другой? Нет ничего проще, универсальный алгоритм помещается в 5 строк:

template<class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result) {
    while(first != last) *result++ = *first++;
    return result;
}
Возможно вы узнали реализацию std::copy с cplusplus.com. Почему же реализация std::copy из GNU STL занимает 139 строк? Давайте разберемся.
STL - это самая базовая библиотека, используемая всеми нормальными программами на C++. Поэтому она должна предоставлять максимально эффективные реализации алгоритмов. Причем эффективные во многих смыслах:
  • Скорость выполнения
  • Размер генерируемого компилятором кода
  • Потребление памяти
  • Скорость компиляции

Скорость выполнения

Код можно сделать более быстрым, если учитывать особенности типов, которыми инстанциируется шаблон. Например, если тип имеет тривиальный конструктор копирования, его можно копировать побайтно. И если объекты лежат в непрерывной области памяти, вместо многократного вызова конструктора можно использовать старую добрую C функцию memmove, которая может задействовать векторные команды процессора, копирующие данные особенно быстро. Обратите внимание, что реализация std::copy не может использовать memcpy, так как memcpy работает только на не перекрывающихся областях памяти.
Таким образом, мы хотим написать две реализации std::copy: одну быструю, для тривиально копируемых типов, а другую универсальную, для всех остальных.

SFINAE

И тут на помощь приходит приём, известный как "substitution failure is not an error". Если при подстановке шаблонных параметров получается некорректное выражение, это не является ошибкой. Компилятор должен проигнорировать шаблон и продолжить поиск. Важно, что в случае шаблонной функции некорректное выражение должно обнаружиться не в теле функции, когда конкретный шаблон уже выбран и продолжать поиск некуда, а в её прототипе. Самый простой способ это сделать - использовать std::enable_if.

std::enable_if

Сам по себе enable_if очень прост. Это шаблон от интегральной константы и типа. Если константа истинна, то он содержит объявление вложенного типа с именем type, в противном случае он пуст.
    // Define a nested type if some predicate holds.
    // Primary template.
    /// enable_if
    template<bool, typename _Tp = void>
    struct enable_if 
    { };

    // Partial specialization for true.
    template<typename _Tp>
    struct enable_if<true, _Tp>
    { typedef _Tp type; };
Тип
std::enable_if<условие, Type>::type
будет определен и иметь тип Type, но только если условие истинно. Используют его вот так:
// Компилятор всегда проигнорирует этот шаблон
template<class T>
std::enable_if<false, T>::type get_t() {return T();}

// А этот будет инстанциирован
template<class T>
std::enable_if<true, T>::type get_t() {return T();}

type traits

Чтобы все описанное выше имело смысл, нужно иметь константы, зависящие от типа. Они называются type traits. Это шаблонные структуры, у которых есть статическое константное публичное свойство value, принимающее значение true или false, в зависимости от типа, которым инстанциирован шаблон. Некоторые из них описаны с помощью частичной специализации шаблона, некоторые реализуются самим компилятором, а некоторые построены как выражения над другими type traits.

Специальная реализация std::copy

Добавим перед универсальной реализацией вот такую:
template<class T>
// В gcc 4.5 нет std::is_trivially_copiable, поэтому используется более сильное условие
//inline typename std::enable_if<std::is_trivially_copiable<T>::value, T*>::type 
inline typename std::enable_if<std::is_trivial<T>::value, T*>::type 
    copy(T* first, T* last, T* result) {

    const ptrdiff_t num = last - first;
    memmove(result, first, sizeof(T) * num);
    return result + num;
}
Если copy будет вызван с тремя указателями на тривиально копируемый тип, то компилятор применит эту оптимизированную реализацию. В противном случае, этот шаблон будет проигнорирован и будет использована универсальная версия.
В реальной библиотеке все немного сложнее, потому что стандартом зафиксировано, что std::copy имеет два шаблонных параметра. Если программист явно их укажет, то все равно надо выбрать оптимизированный вариант. Поэтому различные реализации описаны под служебными именами, а в самом std::move находится код, выбирающий наиболее подходящую реализацию. Вот значительно упрощенный вариант:
#include <type_traits>
#include <cstring>

// Внимание: этот файл собирается только C++11.
// В C++03 нет необходимых type_traits (по крайней мере в публичном интерфейсе)

// Вспомогателльный класс с двумя частичными специализациями. 
// Если is_simple, то применяется memmove, а итераторы считаются указателями.
// В противном случае применяется общий алгоритм.
template<bool is_simple>
struct __do_copy;
// Обратите внимание на название, начинающееся с _. Такие названия зарезервированы
// стандартом для внутреннего использования компилятором и стандартной библиотекой.
// Никогда не используйте такие названия с своих программах, в том числе для приватных
// членов класса. Тут оно использовано так как приводится упрощенная реализация std::copy

template<>
struct __do_copy<true> {
    // Реализация для тривиально копируемых типов
    // Эта функция для внутреннего пользования, поэтому не содержит никаких проверок
    // что memmove действительно применим.
    template<class InputIterator, class OutputIterator>
    static inline OutputIterator do_copy(InputIterator first, InputIterator last, OutputIterator result) {
        typedef typename std::iterator_traits<InputIterator>::value_type ValueType;
        const std::ptrdiff_t num = last - first;
        memmove(result, first, sizeof(ValueType) * num);
        return result + num;
    }
};
    
template<>
struct __do_copy<false> {
    // Универсальная реализация
    template<class InputIterator, class OutputIterator>
    static inline OutputIterator do_copy(InputIterator first, InputIterator last, OutputIterator result) {
        while(first != last) *result++ = *first++;
        return result;
    }
};

// Вспомогательная trait структура для сравнения равенства типов
// В публичном интерфейсе STL её нет
template<typename, typename>
struct are_same : public std::false_type {};
template<typename Tp>
struct are_same<Tp, Tp>: public std::true_type {};

template<class InputIterator, class OutputIterator>
inline InputIterator copy(
            InputIterator first, InputIterator last, OutputIterator result) {
    // Без typename копмилятор не может понять, является value_type типом
    // или, скажем, свойством. Они синтаксически неразличимы.
    typedef typename std::iterator_traits<InputIterator>::value_type ValueTypeI;
    typedef typename std::iterator_traits<OutputIterator>::value_type ValueTypeO;

    const bool is_simple = (
        std::is_trivial<ValueTypeI>::value &&
        std::is_pointer<InputIterator>::value &&
        std::is_pointer<OutputIterator>::value &&
        are_same<ValueTypeI, ValueTypeO>::value);

    // Выбираем подходящую реализацию
    return __do_copy<is_simple>::do_copy(first, last, result);
}
Кроме того, в GNU STL применяется еще одна оптимизация: для итераторов случайного доступа применяется цикл for с вычислением количества итераций. Компилятор умеет разворачивать такие циклы, увеличивая быстродействие программы.

Размер генерируемого кода

Обратите внимание, что шаблон, приведенный в начале статьи, для каждого нового типа будет генерировать полностью новый код. Второй же шаблон вырождается в одно вычитание и одно сложение указателей, а реализация memmove общая для всех типов. То есть помимо ускорения программы, уменьшается её размер. Аналогичные трюки могут применяться в контейнерах: части, не зависящие от хранимого типа выносятся из шаблона и используют общую реализацию. Например, в реализации std::list шаблонная структура, хранящая данные, не содержит ничего кроме самих данных. Ссылки на соседей и операции над ними реализованы в базовом не шаблонном классе, от которого она наследуется.

Используйте библиотечные функции

Мораль этой статьи проста: используйте алгоритмы, предоставляемые стандартной библиотекой как можно чаще. Это делает ваши программы эффективнее, понятнее и гибче
Эффективнее потому, что разработчики стандартных библиотек могут применять оптимизации, которые вы не можете позволить себе в обычном коде. Вы же не готовы писать и поддерживать несколько реализаций копирования объектов под десяток платформ? У прикладного программиста на это нет времени.
Понятнее потому, что вы можете писать короткий высокоуровневый код, оперируя примитивами, знакомыми вашим коллегам.
Гибче потому, что вам не надо делать преждевременных оптимизаций и потому, что вы используете более общие алгоритмы, чем вы бы написали сами.

P.S.

Обратите внимание, что std::copy знает, что result не находится между start и finish, но использует функцию memmove, которая вынуждена это проверить и выбрать направление копирования (от начала к концу или от конца к началу). Можно было сделать чуть более оптимальную реализацию, но тогда бы разработчики STL должны были бы поддерживать специальные релизации под каждую из поддерживаемых архитектур. На это уже тратят свое время разработчики glibc. Дублировать эту работу никому не надо.

Ссылки по теме

  
Помощь
Задать вопрос
 программы
 обучение
 экзамены
 компьютеры
Бесплатный звонок
ICQ-консультанты
Skype-консультанты

Общая справка
Как оформить заказ
Тарифы доставки
Способы оплаты
Прайс-лист
Карта сайта
 
Бестселлеры
Курсы обучения "Atlassian JIRA - система управления проектами и задачами на предприятии"
Microsoft Windows 10 Профессиональная 32-bit/64-bit. Все языки. Электронный ключ
Microsoft Office для Дома и Учебы 2019. Все языки. Электронный ключ
Курс "Oracle. Программирование на SQL и PL/SQL"
Курс "Основы TOGAF® 9"
Microsoft Office 365 Персональный 32-bit/x64. 1 ПК/MAC + 1 Планшет + 1 Телефон. Все языки. Подписка на 1 год. Электронный ключ
Курс "Нотация BPMN 2.0. Ее использование для моделирования бизнес-процессов и их регламентации"
 

О нас
Интернет-магазин ITShop.ru предлагает широкий спектр услуг информационных технологий и ПО.

На протяжении многих лет интернет-магазин предлагает товары и услуги, ориентированные на бизнес-пользователей и специалистов по информационным технологиям.

Хорошие отзывы постоянных клиентов и высокий уровень специалистов позволяет получить наивысший результат при совместной работе.

В нашем магазине вы можете приобрести лицензионное ПО выбрав необходимое из широкого спектра и ассортимента по самым доступным ценам. Наши менеджеры любезно помогут определиться с выбором ПО, которое необходимо именно вам. Также мы проводим учебные курсы. Мы приглашаем к сотрудничеству учебные центры, организаторов семинаров и бизнес-тренингов, преподавателей. Сфера сотрудничества - продвижение бизнес-тренингов и курсов обучения по информационным технологиям.



 

О нас

 
Главная
Каталог
Новинки
Акции
Вакансии
 

Помощь

 
Общая справка
Как оформить заказ
Тарифы доставки
Способы оплаты
Прайс-лист
Карта сайта
 

Способы оплаты

 

Проекты Interface Ltd.

 
Interface.ru   ITShop.ru   Interface.ru/training   Olap.ru   ITnews.ru  
 

119334, г. Москва, ул. Бардина, д. 4, корп. 3
+7 (495) 229-0436   shopadmin@itshop.ru
Проверить аттестат
© ООО "Interface Ltd."
Продаем программное обеспечение с 1990 года