четверг, 20 ноября 2014 г.

Как устроен weak_ptr

Для чего нужен.

В предыдущем посте я рассказывал про устройство shared_ptr. К сожалению, он подвержен классической проблеме циклических ссылок. Для решения этой проблемы используется weak_ptr.

Как решить проблему циклических ссылок.

В объект счётчика ссылок добавляется счётчик слабых ссылок. weak_ptr увеличивает этот счётчик при создании и копировании. При этом объект счётчика ссылок не удаляется до тех пор, пока есть хотя бы один shared_ptr или один weak_ptr, т.к. weak_ptr необходимо обращаться к счётчику ссылок даже если нет ни одного живого shared_ptr. Но сам объект, на который указывает shared_ptr, может быть удалён, если больше не осталось живых shared_ptr.
Код объекта счётчика ссылок теперь будет выглядеть так:
template <typename T>
class sp_counted {
public:
    explicit sp_counted(T *p) noexcept
        : shared_count(1),
          weak_count(1),
          ptr(p) {}

    void add_ref() noexcept {
        ++shared_count;
    }

    void release() noexcept {
        if (!--shared_count) {
            // Если последний shared_ptr удалился, удаляем объект
            delete ptr;
            if (!--weak_count) {
                // Если нет слабых ссылок, удаляем объект счётчика
                destroy();
            }
        }
    }

    void add_ref_weak() noexcept {
        ++weak_count;
    }

    void release_weak() noexcept {
        if (!--weak_count) {
            // Если последний weak_ptr удалился, удаляем объект счётчика.
            // Т.к. shared_ptr тоже увеличивает weak_count при создании,
            // нет необходимости проверять значение shared_count
            destroy();
        }
    }

    size_t use_count() const noexcept {
        return shared_count.load();
    }

    // Попытка увеличить счётчик shared_count из weak_ptr
    // Потокобезопасен, lock-free
    void add_ref_lock() {
        // Сохраняем текущее значение shared_count
        size_t cur_value(shared_count.load());
        do {
            // Если счётчик сильных ссылок равен нулю (т.е. нет больше живых shared_ptr),
            // то новый shared_ptr создавать не из чего.
            if (!cur_value) {
                throw std::bad_weak_ptr();
            }
          // Пытаемся увеличить счётчик shared_count на единицу
          // Если в промежутке между сохранением shared_count в cur_value, shared_count изменился,
          // то операция compare_exchange_weak вернёт false, запишет новое значение shared_count в cur_value,
          // и цикл повторится
        } while (shared_count.compare_exchange_weak(cur_value, cur_value + 1));
    }

private:
    void destroy() noexcept {
        delete this;
    }

private:
    // Счётчик ссылок shared_ptr
    std::atomic<size_t> shared_count;
    // Счётчик ссылок weak_ptr
    std::atomic<size_t> weak_count;
    T *ptr;
};
Вопрос: почему вызов add_ref_lock безопасен? Ведь в другом потоке shared_ptr в деструкторе может удалить объект sp_counted, и обращение к переменной shared_count будет некорректным.
Ответ: потому что деструктор shared_ptr не удалит объект sp_counted, т.к. есть живая слабая ссылка (weak_count != 0).
Код weak_ptr будет выглядеть так:

template <typename T>
class weak_ptr {
    friend class shared_ptr<T>;
public:
    weak_ptr() noexcept : ptr(nullptr), counted(nullptr) {}

    weak_ptr(const weak_ptr &other) noexcept : ptr(other.ptr), counted(other.counted) {
        add_ref_weak();
    }

    weak_ptr(const shared_ptr<T> &p) noexcept : ptr(p.ptr), counted(p.counted) {
        add_ref_weak();
    }

    weak_ptr &operator=(const weak_ptr &other) noexcept {
        release_weak();

        ptr = other.ptr;
        counted = other.counted;

        add_ref_weak();

        return *this;
    }

    weak_ptr &operator=(const shared_ptr<T> &p) {
        release_weak();

        ptr = p.ptr;
        counted = p.counted;

        add_ref_weak();

        return *this;
    }

    // Пытаемся сделать shared_ptr. Для этого вызывается конструктор shared_ptr(const weak_ptr &amp;);
    // В случае невозможности создать shared_ptr возвращается пустой объект
    shared_ptr<T> lock() noexcept {
        try {
            return shared_ptr<T>(*this);
        } catch (const std::bad_weak_ptr &) {
            return shared_ptr<T>();
        }
    }

    size_t use_count() const noexcept {
        return counted != nullptr ? counted->use_count() : 0;
    }

private:
    void add_ref_weak() noexcept {
        if (counted) {
            counted->add_ref_weak();
        }
    }

    void release_weak() noexcept {
        if (counted) {
            counted->release_weak();
        }
    }

private:
    T *ptr;
    sp_counted<T> *counted;
};
В код shared_ptr добавляется конструктор shared_ptr(const weak_ptr &p):
template <typename T>
class weak_ptr;

template <typename T>
class shared_ptr {
    friend class weak_ptr<T>;
public:
    shared_ptr() noexcept : ptr(nullptr), counted(nullptr) {}

    // excaption safe конструктор
    explicit shared_ptr(T *p) {
        std::unique_ptr<T> holder(p);
        // new может кинуть исключение, и если p не передать в unique_ptr,
        // память под p потеряется
        counted = new sp_counted<T>(holder.get());
        ptr = holder.release();
    }

    ~shared_ptr() noexcept {
        release();
    }

    shared_ptr(const shared_ptr<T> &other) noexcept : ptr(other.ptr), counted(other.counted) {
        add_ref();
    }

    shared_ptr(const weak_ptr<T> &p) : ptr(p.ptr), counted(p.counted) {
        if (counted) {
            // Пытаемся увеличить счётчик ссылок объекта
            // В случае неудачи сгенерируется исключение std::bad_weak_ptr()
            counted->add_ref_lock();
        } else {
            throw std::bad_weak_ptr();
        }
    }

    shared_ptr &operator=(const shared_ptr<T> &other) noexcept {
        // Освобождаем владение предыдущим указателем
        release();

        // Выполняем присваивание
        ptr = other.ptr;
        counted = other.counted;

        // Устанавливаем владение новым указателем
        add_ref();

        // Ура! Я не забыл вернуть *this!
        return *this;
    }

    T *get() const noexcept {
        return ptr;
    }

    size_t use_count() const noexcept {
        return counted != nullptr ? counted->use_count() : 0;
    }

private:
    void add_ref() noexcept {
        if (counted) {
            counted->add_ref();
        }
    }

    void release() noexcept {
        if (counted) {
            counted->release();
        }
    }

private:
    T *ptr;
    sp_counted<T> *counted;
};

А что с потокобезопасностью?

Гарантии потокобезопасности такие же как и у shared_ptr. Но на всякий случай повторю: любая операция с weak_ptr потокобезопасна, если каждый поток хранит копию weak_ptr. При этом каждая копия может хранить сслыку на один shared_ptr. Хранить ссылки на один weak_ptr в разных потоках непотокобезопасно.

среда, 19 ноября 2014 г.

Как устроен shared_ptr

Для чего нужен

shared_ptr нужен для ситуации, когда имеются несколько ссылок на объект, созданных в динамической памяти. При этом освобождать объект нужно тогда, когда удаляется последняя ссылка. И делать это нужно безопасно даже в многопоточном коде.

Как отслеживать ссылки на объект

Каждый объект shared_ptr содержит созданный в динамической памяти счётчик ссылок, который увеличивается на единицу, когда shared_ptr завладевает объектом, и уменьшается на единицу, когда shared_ptr перестаёт владеть объектом.
Код объекта счётчика ссылок может выглядеть так:

template <typename T>
class sp_counted {
public:
    explicit sp_counted(T *p) noexcept : count(1), ptr(p) {}

    void add_ref() noexcept {
        ++count;
    }

    void release() noexcept {
        if (!--count) {
            delete ptr;
            delete this;
        }
    }

    size_t use_count() const noexcept {
        return count.load();
    }

private:
    std::atomic<size_t> count;
    T *ptr;
};
При этом код shared_ptr может выглядеть так:
template <typename T>
class shared_ptr {
public:
    shared_ptr() noexcept : ptr(nullptr), counted(nullptr) {}

    // excaption safe конструктор
    explicit shared_ptr(T *p) {
        std::unique_ptr<T> holder(p);
        // new может кинуть исключение, и, если p не передать в unique_ptr,
        // память под p потеряется
        counted = new sp_counted<T>(holder.get());
        ptr = holder.release();
    }

    ~shared_ptr() noexcept {
        release();
    }

    shared_ptr(const shared_ptr &other) noexcept : ptr(other.ptr), counted(other.counted) {
        add_ref();
    }

    shared_ptr &operator=(const shared_ptr &other) noexcept {
        // Освобождаем владение предыдущим указателем
        release();

        // Выполняем присваивание
        ptr = other.ptr;
        counted = other.counted;

        // Устанавливаем владение новым указателем
        add_ref();

        // Ура! Я не забыл вернуть *this!
        return *this;
    }

    T *get() const noexcept {
        return ptr;
    }

    size_t use_count() const noexcept {
        return counted != nullptr ? counted->use_count() : 0;
    }

private:
    void add_ref() {
        if (counted) {
            counted->add_ref();
        }
    }

    void release() {
        if (counted) {
            counted->release();
        }
    }

private:
    T *ptr;
    sp_counted<T> *counted;
};

А что с потокобезопасностью?

Любая операция с shared_ptr потокобезопасна, если каждый поток хранит копию shared_ptr. При этом каждая копия может хранить указатель на один объект.
Хранить ссылки на один shared_ptr в разных потоках непотокобезопасно.

вторник, 18 ноября 2014 г.

Разработка модулей для nginx

Отличное руководство по разработке модулей для nginx - здесь

воскресенье, 9 ноября 2014 г.

Алгоритмы кэширования

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

OPT

Теоретически оптимальный алгоритм. Вытесняет тот элемент, который не будет запрашиваться дольше всех в будущем. На практике он не реализуем, т.к. невозможно знать будущее, но используется для того, чтобы получить оценку максимально возможного кэш хита по конкретному логу запросов.

FIFO

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

LFU

Каждый элемент имеет счётчик обращений. Новый элемент вставляется в кэш со значением счётчика равным 1. При попадании в кэш счётчик найденного элемента увеличивается на 1. Если нужно освободить место, нужно найти элемент с самым маленьким значением счётчика. Таким образом вытесняется тот элемент, которые запрашивался реже всего.
Главный недостаток LFU состоит в том, что некогда частотные элементы могут присутствовать в кэше очень долго, даже если они уже давно не запрашивались.
Реализация.

LRU

Новый элемент вставляется в голову списка. При попадании в кэш элемент перемещается в голову списка. Если нужно освободить место, вытесняется элемент из хвоста списка. Таким образом вытесняется тот элемент, которые не запрашивался дольше всех.
Главный недостаток LRU состоит в том, что он не устойчив к линейному чтению, т.к. оно вымывает все "прогретые" данные.
Реализация.

Сегментный LRU (SNLRU)

Один из недостатков LRU в том, что если мы вставили новый элемент в кэш, и этот элемент больше не будет никогда запрошен, ему придётся пройти через весь список, пока он не будет вытеснен. Сегментный LRU решает эту проблему. Кэш организован ввиде нескольких (N) LRU кэшей. Новый элемент вставляется в нулевой LRU кэш. При попадании в кэш элемент перемещается в следующий LRU кэш, либо на MRU (Most Recently Used) позицию последнего LRU кэша, если выше уже идти некуда. При вытеснении элемента из k-го LRU кэша он перемещается в k-1 LRU кэш. По достижению нулевого LRU кэша элемент удаляется.
Алгоритм устойчив к линейному чтению, т.к. новые элементы не могут вытеснить прогретые. При этом он обладает свойством LFU алгоритма - элементы перемещается в следующий список при каждом попадании в кэш. Более того, если взять N достаточно большим, то получится LFU кэш. При этом сегментный LRU обладает тем же недостатком LFU - частотные в прошлом элементы будут долго сидеть в кэше прежде чем вымоются оттуда.
Реализация.

Mid point LRU

Является вариацией сегментного LRU, в котором количество сегментов - 2, и эти два сегмента делят кэш не пополам, а в некоторой пропорции. Размеры сегментов подбирается эмпирически и зависит от характера потока данных. Этот алгоритм очень просто реализуется и на деле показывает один из лучших результатов. Обладает всеми достоинствами сегментного LRU.
Реализация.

2Q

Кэш разделяется на три части:
  • In - FIFO кэш, в который попадают все новые элементы;
  • Out - FIFO кэш, в который попадают элементы, вытесненные из In. При этом этот кэш хранит ключ и не хранит значение, поэтому его можно сделать достаточно большим;
  • Main - главный LRU кэш, в который попадают новые элементы найденные в Out. При вытеснении элемента из главного кэша он удаляется.
Алгоритм устойчивость к линейному чтению.
Оригинальная статья с описаниемреализация.

MQ

По сути является сегментным LRU но с некоторыми существенными улучшениями.
Для каждого элемента ведётся счётчик его запросов. Номер LRU кэша, в который стоит поместить элемент вычисляется используя некоторую функцию от этого счётчика, например логарифм.
Возможна ситуация, когда элемент, который вытесняется из кэша, в ближайшем будущем может стать очень популярным, поэтому имеет смысл как-то запоминать позицию, с которой он был вытеснен, и при следующем добавлении в кэш этого элемента сразу ставить его на позицию с учётом его "прошлых заслуг". Для этого по аналогии с 2Q алгоритмом заводится FIFO кэш, который хранит ключ элемента и позицию, с которой элемент был вытеснен. Если новый добавляемый элемент находится в этом FIFO кэше, он сразу добавляется в позицию, с который был вытеснен в прошлом.
Алгоритм обладает также свойсвами LFU. Чтобы решить проблему элементов, которые часто запрашивались в прошлом, а сейчас просто занимают место, каждый элемент хранит время, когда к нему было обращение. Далее в какие-то моменты времени проверяются элементы на LRU позиции всех сегментов, и если к этим элементам давно не было оращения, они перемещаются в сегмент нижнего уровня, либо удаляются из кэша если находятся на нулевом сегменте.

Графики:


В легенде алгоритмы отсортированы от лучшего к худшему.
MQ* - MQ алгоритм, в котором не выполняется проверка времени жизни объекта в кэше.
На графике видно, что все существующие алгоритмы можно ещё оптимизировать, т.к. до теоретически оптимального есть ещё около 10%.

пятница, 7 ноября 2014 г.

Специальные функции-члены в C++

Здесь есть хорошее описание специальных функции-членов С++ от Howard Hinnant (один из основных комиттеров в Clang STL). Специальные функции-члены - это те, которые компилятор генерирует, если они не объявлены явно:

  • Конструктор по умолчанию (Default constructor);
  • Деструктор (Destructor);
  • Конструктор копирования (Copy constructor);
  • Оператор копирующего присваивания (Copy assignment);
  • Конструктор переноса (Move constructor);
  • Оператор переносящего присваивания (Move assignment);
Или в коде:
struct MyStruct {
    MyStruct() = default;
    ~MyStruct() = default;
    MyStruct(const MyStruct &) = default;
    MyStruct &operator=(const MyStruct &) = default;
    MyStruct(MyStruct &&) = default;
    MyStruct &operator=(MyStruct &&) = default;
};
Каждая из этих функций может быть:
  • Не объявлена;
  • Может быть сгенерирована компилятором:
    • Как default - вызывает соответствующие функции для членов. Для конструктора - конструкторы членов, для оператора присваивания - операторы присваивания членов и т.д.
    • Как delete - если соответствующая функция у члена объявлена как delete или функция не может быть сгенерирована. Например для оператора присваивания член может иметь оператор присваивания объявленный как delete, или объект не может копироваться, т.к. содержит ссылку.
  • Может быть объвлена явно программистом:
    • Как default;
    • Как delete;
    • С телом функции;
Разница между "не объявлена" и объявлена с delete в том, что не объявленная функция не участвует в разрешении перегрузки.

Например класс:
struct MyStruct {
    template <typename... Args>
    MyStruct(Args...) {
    }
};
может быть создан вызовом конструктора без параметров, т.к. конструктор по умолчанию не объявлен, и не участвует в разрешении перегрузки.
Но такой класс:

struct MyStruct {
    template <typename... Args>
    MyStruct(Args...) {
    }
    MyStruct() = delete;
};
создать вызовом конструктора без параметров не получится, т.к. конструктор по умолчанию объявлен и участвует в разрешении перегрузки, и при этом он объявлен как delete.
При этом стоить помнить одно правило - если перемещающие функции объявленны компиляторм как delete, они не участвуют в разрешении перегрузки, не смотря на факт объвления.
Когда и какие функции компилятор генерирует сам, а когда объявляет их как delete?
Объявленный конструктор по умолчанию - остальные 5 функций объявляются компилятором как default:
struct MyStruct {
    MyStruct() = default;
    ~MyStruct() = default;
    MyStruct(const MyStruct &) = default;
    MyStruct &operator=(const MyStruct &) = default;
    MyStruct(MyStruct &&) = default;
    MyStruct &operator=(MyStruct &&) = default;
};
Объявленный деструктор - конструктор перемещения и перемещающий оператор присваивания становятся не объявленными, остальные функции объявляются компилятором как default:
struct MyStruct {
    MyStruct() = default;
    ~MyStruct() = default;
    MyStruct(const MyStruct &) = default;
    MyStruct &operator=(const MyStruct &) = default;


};
Объявленный конструктор копирования - конструктор по умолчанию, конструктор перемещения и перемещающий оператор присваивания становятся не объявленными, остальные функции объявляются компилятором как default:
struct MyStruct {

    ~MyStruct() = default;
    MyStruct(const MyStruct &) = default;
    MyStruct &operator=(const MyStruct &) = default;


};
Объявленный копирующий оператор присваивания - конструктор перемещения и перемещающий оператор присваивания становятся не объявленными, остальные функции объявляются компилятором как default:
struct MyStruct {
    MyStruct() = default;
    ~MyStruct() = default;
    MyStruct(const MyStruct &) = default;
    MyStruct &operator=(const MyStruct &) = default;


};
Объявленный конструктор перемещения - конструктор по умолчанию и перемещающий оператор присваивания становятся не объявленным, деструктор объявляется компилятором как default, остальные функции объявляются компилятором как delete.
struct MyStruct {

    ~MyStruct() = default;
    MyStruct(const MyStruct &) = delete;
    MyStruct &operator=(const MyStruct &) = delete;
    MyStruct(MyStruct &&) = default;

};
Объявленный перемещающий оператор присваивания - конструктор по умолчанию и деструктор объявляется компилятором как default, конструктор копирования и оператор копирующего присваивания объявляются компилятором как delete, конструктор перемещения становится не объявленным.
struct MyStruct {
    MyStruct() = default;
    ~MyStruct() = default;
    MyStruct(const MyStruct &) = delete;
    MyStruct &operator=(const MyStruct &) = delete;

    MyStruct &operator=(MyStruct &&) = default;
};

четверг, 30 октября 2014 г.

Презентация Facebook на ВМК МГУ

28 октября в на ВМК МГУ ребята из Facebook рассказывали про устройство Facebook TAO - хранилище социального графа.
Небольшая выжимка:

  • Социальный граф - набор объектов и связи между ними. Например: пост связан с пользователем, фотография к посту связана с постом, коментарии и лайки других пользователей связаны с постом, пользователи могут иметь отношение дружбы, и т.д.
  • Чтобы отрисовать страницу пользователя, нужно загрузить все ноды графа, связанные с последним событием;
  • На загрузку одной ноды графа отводится 1 милисекунда;
  • Особенность кеширования - наиболее горячие данные те, которые недавно создались;
  • У каждого объекта графа есть уникальный 64 битный глобальный идентификатор. Уникальность гарантируется так: в идентификаторе объекта N бит отводится под идентификатор пользователя, остальные биты под событие. Главное для каждого пользователя уметь получить следующий уникальный идентификатор события. Это не сложно и не требует глобальной синхронизации.
  • В качестве хранилища свой отбранчёванный MySQL с InnoDB;
  • Многоуровневое кеширование c Memcached и McRouter;
  • Есть процедура прогревания кеша;
  • 99.8% - чтение данных, 0.2% - запись;
  • 3 датацентра в США, 1 в Европе;
  • В каждом ДЦ есть мастер нода MySQL. Все записи идут в мастер ноду, далее асинхронно реплицируются;
  • Если мастер нода падает, выбирается новая мастер нода;
  • Чтобы не задолбать бэкенд, над каждым инстансом MySQL располагается Master Cache, через который идут все запросы в этот инстанс. Это позволяет контролировать нагрузку на  MySQL;
  • Очень большой кеш-хит, поэтому до бэкенда MySQL долетает мало запросов (около 1 KRPS на инстанс);
  • Если все кеши отвалятся, MySQL бекенд нагрузку не выдержит;
  • Для проведения аналитики по социальному графу есть выгрузка данных в Hive;
  • Шардируют базу по диапазонам идентификатора, поэтому процедуры перешардирования нет;
  • Есть региональные прокси для снижения латентности https handshake;
Более подробно можно почитать в статье

понедельник, 20 октября 2014 г.

Материалы по модели памяти C++

1. C++ and Beyond 2012: Herb Sutter - atomic<> Weapons Part 1
2. C++ and Beyond 2012: Herb Sutter - atomic<> Weapons Part 2
3. C++ Concurrency in Action: Practical Multithreading (Chapter 5)
4. Pershing on programming
5. Threads Cannot be Implemented as a Library
6. Threads and memory model for C++ by Hans Boehm

C++ Type Deduction

На конференции cppcon 2014 был доклад Скота Мейерса про выведение типов - Type Deduction and Why You Care (слайды).
Небольшая шпаргалка:

#include <iostream>

template <typename T>
T func1(T f) {
    return f;
}

template <typename T>
T func2(T& f) {
    return f;
}

template <typename T>
T func3(const T& f) {
    return f;
}

template <typename T>
T func4(T&& f) {
    return f;
}

void func5(const int * const param1,
           const int *       param2,
                 int *       param3) {
    auto p1 = param1;   // p1 = const int*; same as for auto *
    auto p2 = param2;   // p2 = const int*; same as for auto *
    auto p3 = param3;   // p2 = int*; same as for auto *

    auto &rp1 = param1; // p1 = const int *const &; same as for auto &&
    auto &rp2 = param2; // p2 = const int *&; same as for auto &&
    auto &rp3 = param3; // p3 = int *&; same as for auto &&
}

int main(int argc, char* argv[]) {
    int x = 10;
    int &rx = x;
    const int &crx = x;

    func1(x);       // T = int; param = int;
    func1(rx);      // T = int; param = int;
    func1(crx);     // T = int; param = int;

    func2(x);       // T = int; param = int&;
    func2(rx);      // T = int; param = int&;
    func2(crx);     // T = const int; param = const int&;

    func3(x);       // T = int; param = const int&;
    func3(rx);      // T = int; param = const int&;
    func3(crx);     // T = int; param = const int&;

    func4(x);       // T = int&; param = int&;
    func4(rx);      // T = int&; param = int&;
    func4(crx);     // T = const int&; param = const int&;
    func4(42);      // T = int; param = int&&;

    return 0;
}

auto в range base loop

На всех конференциях посвящённых C++ практически всегда рекомендуют использовать для определения переменных auto. Один из доводов в пользу auto, кроме того, что приходится меньше писать, в том, что сгенерированный код быстрее, т.к. всегда выводится корректный тип, и нет места приведению типов. Хотя кажется что тип всегда и так очевиден возможны ситуации, когда по невнимательности типы не совпадают, и компилятор выполняет конвертацию.
Например:
#include <map>
#include <string>

int main() {
  std::map<std::string, int> m;
  for(const auto &a : m) {}
  for(const std::pair<std::string, int> &a : m) {}
  return 0;
}
Вопрос: Какой вариант for быстрее? Ответ: Первый, т.к. auto& выводит корректный тип элемента, который для std::map<std::string, int> - std::pair<const std::string, int>, т.к. для std::map запрещено модифицировать ключ. Для второго for-а создаётся временный объект std::pair<std::string, int>, поэтому он работает медленнее.

суббота, 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.

HTML кодировщик для C++ кода

Ссылка

Проверка существоания методов у класса


На cppcon 2014 был доклад для про современное метопрограммирование шаблонов - слайды.
Там в частности рассказывалось как реализовать проверку существует ли для класса оператор копирующего присваивания. Суть метода в том, чтобы получить тип результата некоторого выражения (в данном случае оператора присваивания) через decltype, и инстанцировать этим типом шаблон. Если выражение корректно (то есть у нас есть оператор присваивания), то результатом параметризации будет один тип (например std::true_type), если нет, то другой тип (например std::false_type).

Пример кода из слайдов:
template< class T >
using copy_assign_t = decltype(declval<T&>() = declval< T const& >());

template<class T>
struct is_copy_assignable {
private:
  template< class U, class = copy_assign_t<U> >
  static true_type try_assign( U&& ); // SFINAE may apply!
  static false_type try_assign(...); // catch-all overload
public:
  using type = decltype(try_assign(declval<T>()));
};

Работает это всё благодаря SFINAE.
Я попробовал написать общий класс, который возвращает true_type/false_type в зависимости от того, является ли любое переданное выражение корректным. Его можно использовать, чтобы реализовать свои проверки для типов, при этом не требуется писать много кода.
Например нужно проверить, есть ли в классе нестатическая функция get_value():
//Объявляем тип выражения
template <typename T>
using has_get_value_t = decltype(std::declval<T>().get_value());

template <typename T>
struct has_get_value : is_correct_expression_t<T, has_get_value_t> { };

int main() {
  std::cout << std::is_same<std::true_type, has_get_value<MyClass>::type>::value << std::endl;
}
Как это реализовано:
template <typename T, template<class> class E>
struct is_correct_expression_t {
private:
    template <typename U, typename = E<U>>
    static std::true_type try_evaluate(U&&);
    static std::false_type try_evaluate(...);
public:
    using type = decltype(try_evaluate(std::declval<T>()));
};
Для move/copy assign будет выглядеть так:
template <typename T>
using copy_assign_t = decltype(std::declval<T&>() = std::declval<const T&>());

template <typename T>
struct is_copy_assignable : is_correct_expression_t<T, copy_assign_t> { };

template <typename T>
using move_assign_t = decltype(std::declval<T&>() = std::declval<T&&>());

template <typename T>
struct is_move_assignable : is_correct_expression_t<T, move_assign_t> { };