Std:: function вместо шаблонов для предикатов

Многие стандартные библиотечные алгоритмы используют предикатные функции. Однако тип этих предикатов является произвольным параметром шаблона, предоставленным пользователем. Почему С++ 11 не указывает, что они принимают определенный тип, например std::function? Например:

template< class InputIt >
InputIt find_if( InputIt first, InputIt last,
             std::function<bool()> p );

Не использует это вместо шаблона в качестве типа аргумента не намного чище?

Ответ 1

std::function предназначен для полиморфизма во время выполнения. Любой конкретный экземпляр std::function мог бы хранить функтор любого типа (тип, подходящий для подписи std::function, конечно).

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

Важнее всего, чтобы такие алгоритмы не требовали для вас затрат на полиморфизм времени выполнения. Если вам нужен полиморфизм во время выполнения, вы можете отправить его std::function или что угодно. Если вы хотите использовать полиморфизм времени компиляции, вы предоставляете ему тип, который не использует полиморфную отправку (aka: большинство функторов или функций).

Стоимость полиморфизма во время выполнения также включает неспособность встроить вызов функции. Используя надлежащий функтор (или даже указатели на функции, в зависимости от того, насколько хорош ваш компилятор), компилятор обычно может встроить вызов функции, если он этого пожелает. С полиморфизмом во время выполнения вы не только оплачиваете стоимость отправки во время выполнения (что может включать дополнительные затраты на пересылку параметров), вы также теряете важные возможности оптимизации.

Ответ 2

Производительность

Базовая функция шаблона очень хороша, чем режим std::function. Я сделал этот тест для вас:

template <typename F>
void test1(const F &f)
{
    for (unsigned long long i = 0; i < 19000000; i++)
        f();
}

void test2(function<void()> &f)
{
    for (unsigned long long i = 0; i < 19000000; i++)
        f();
}

int main()
{
    {
    LARGE_INTEGER frequency, start, end;
    double interval;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&start);

    unsigned long long x = 0;
    test1([&x]()
    {
        x++;
    });

    QueryPerformanceCounter(&end);
    interval = (double) (end.QuadPart - start.QuadPart) / frequency.QuadPart;

    cout << "Template mode: " << interval << " " << x << endl;
    }
    {
    LARGE_INTEGER frequency, start, end;
    double interval;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&start);

    unsigned long long x = 0;
    function<void() > f = [&x]()
    {
        x++;
    };
    test2(f);

    QueryPerformanceCounter(&end);
    interval = (double) (end.QuadPart - start.QuadPart) / frequency.QuadPart;

    cout << "std::function mode:" << interval << " " << x << endl;
    }
}

Режим шаблона: 2.13861e-006

std:: function mode: 0.220006

gcc 4.7.2 на Windows7 -O2 Core2 Duo CPU 2.40 ГГц

Ответ 3

Потому что std::function несовершенен.

Как это несовершенно? Позвольте мне перечислить пути.

{

Во-первых, std::function не поддерживает идеальное перемещение любых объектов, переданных ему. И на практике это невозможно. std::function предоставляет одну фиксированную подпись для вызывающих абонентов и может принимать множество видов вызовов, а для совершенной пересылки требуется специальная подпись для каждого вызывающего абонента и вызываемого абонента. Он поддерживает идеальную пересылку точно аргументов, которые он предоставляет в своей подписи, но этого недостаточно.

Представьте себе std::function, который принимает два аргумента и int и a double. Для идеальной пересылки ему приходилось принимать int&, int&& и int const&, то же самое, что и для double (не говоря уже об изменчивых переменных). Количество подписей, которые каждый std::function должен принимать, чтобы снять идеальную пересылку, экспоненциально растет с количеством аргументов, которые она имеет. std::function набор подписей, которые он предоставляет (в настоящее время 1), фиксируется при создании экземпляра, а набор шаблонов, который он предоставляет, неограничен и генерируется только при использовании. Это важно, потому что некоторые функционально-подобные объекты по-разному оптимизируют для этих случаев! Таким образом, каждый std::function вы удалили возможности идеально переадресовывать вызовы на завернутый тип.

Вторая причина, по которой std::function несовершенна, заключается в том, что компиляторы сосут. Если вы обернете лямбда в std::function, а затем вызовите с ним алгоритм, теоретик может понять, что этот std::function обертывает некоторую фиксированную лямбду, но на практике он теряет следы этого факта и рассматривает std::function как обертка вокруг некоторого общего класса. Таким образом, даже в тех случаях, когда подпись std::function точно соответствует вариантам использования алгоритма, препятствуя тому, чтобы узкое место std::function затрудняло переадресацию, на практике будут накладные расходы из-за выполненного типа стирания на std::function, и компилятору будет трудно оптимизировать над std::function вызовом "барьер".

Третья причина, по которой std::function несовершенна, заключается в том, что она побуждает авторов алгоритмов чрезмерно ограничивать, какие параметры могут быть переданы в алгоритмах. Если вы исследуете find_if, наивное предположение состоит в том, что то, что вы ищете, должно быть того же типа, что и типы, хранящиеся в контейнере, или, по крайней мере, конвертируемые: но алгоритм std::find_if требует только того, чтобы они были сопоставимы под передается в функторе.

Это позволяет вам писать функторы с поддержкой нескольких типов и передавать целевой объект, который имеет несвязанный тип, в тип на контейнере, и все работает отлично. Большинство людей не нуждаются в этом, и их код работает без него - и это хорошо.

Наивный std::find_if будет извлекать базовый тип контейнера, а функция сравнения будет находиться между парами этого типа - или это будет 4-стороннее сравнение между типами контейнеров и типом вещи, которую ищут для. В одном случае мы теряем гибкость - в другом случае каждый платит за странный угловой случай. И на С++ вы должны платить только за те функции, которые вам нужны, когда они вам понадобятся!

Четвертая причина, по которой std::function несовершенна, заключается в том, что она в основном является инструментом стирания типа. Это детали реализации, но я не знаю компилятора, который далеко уходит от них. Цель std::function состоит в том, чтобы показать единственную подпись и возвращаемое значение и сказать: "Я могу хранить все, что соответствует этой сигнатуре, и это возвращаемое значение, и вы можете назвать это". Он предоставляет статический интерфейс времени выполнения и реализацию для выполнения этой задачи. Когда вы инициализируете std::function, он выполняет компиляцию с целью создания вспомогательного объекта, который обертывает этот конкретный объект в единый интерфейс std::function, а затем сохраняет его в шаблоне pImpl. Все это - работа, которая не нужна, если вам не требуется стирать стили.

Стандартные алгоритмы касаются написания кода высокого уровня, который почти так же эффективен, как и ручные решения. Даже стоимость вызова функции указателя не требуется для решения большинства из этих проблем, допустим виртуальный вызов с помощью стираемого типа std::function.

}; // enum

std::function - это отличный инструмент для обратных вызовов, для замены универсальных интерфейсов virtual, и когда вам нужно скрыть детали реализации от вызывающего абонента (скажем, вам нужно перекрещивать границы единицы компиляции для проектных решений).

Хорошей новостью является то, что лучшие решения этой проблемы идут по трубе. В частности, одна из целей С++ 14 или С++ 17 заключается в том, что у нее будет какая-то "концептуальная" поддержка, где вы можете сказать, что "этот аргумент шаблона имеет следующие свойства". Точный синтаксис будет далек от определенного - предложение С++ 11 Concept, вероятно, уйдет далеко, но энтузиазма много, и сейчас есть рабочая группа по проблеме.

Когда или если это будет сделано, вы сможете пометить функтора содержательной концептуальной информацией, в которой говорится: "Этот аргумент - это не просто какой-либо тип, а тип, который является функтором, который принимает два значения (включая содержащиеся типы данных) и возвращает bool совместимое значение", которое компилятор, ваша IDE, и вы можете понять, не заходя в документацию для этой функции.