Привет, хабр.

Как-то раз, одним прекрасным воскресным днём писал я код одного своего проекта. Код выглядел как-то так, если упрощать:
const bool exists = WithObject (objectId,
        [] (const Media::IAudioSource*, const QModelIndex&) { return true; },
        [] (const QModelIndex&) { return false; });

WithObject пытается найти некоторый объект по его ID и выполняет первый функтор, если он найден, иначе выполняет второй функтор, если объект не найден. При этом возвращается значение, которое вернул выполненный функтор (подразумевается, что возвращаемый тип второго функтора приводим к типу первого). Функторам при этом передаётся всякая разная полезная информация, полученная в ходе поиска (например, сам объект).

Вышеприведённый код, соответственно, просто проверяет существование объекта, и аргументы, которые WithObject передаёт функторам, оказываются не нужны. Так вот, подумалось мне, неплохо было бы написать такую функцию DropArgs(), чтобы вот такой код
const bool exists = WithObject (objectId,
        DropArgs ([] { return true; }),
        DropArgs ([] { return false; }));
был корректным. Или, что то же самое, чтобы можно было писать DropArgs ([] { return false; }) (0, 3.14, "foobar");.
А если нужны только первые N аргументов, остальные тоже можно было не указывать: DropArgs ([] (int n) { return n; }) (0, 3.14, "foobar");.

Думать будем на C++14, по возможности пытаясь сохранить совместимость с C++11. На самом деле, из C++14 понадобится только вывод возвращаемого типа функций, да и, читаемости ради, всякие std::result_of_t<> вместо typename std::result_of<>::type, так что у нас есть все шансы.

Итак, начнём с самой функции DropArgs(). Разумно предположить, что функция должна принимать некоторый произвольный функтор и возвращать некоторую обёртку с переопределённым operator(), принимающим произвольное число аргументов. Сформулируем это:
namespace detail
{
    template<typename F>
    class Dropper
    {
        F F_;
    public:
        Dropper (const F& f)
        : F_ (f)    // можно было бы написать F_ { f }, но clang <3.6 обижается
        {
        }

        template<typename... Args>
        auto operator() (Args&&... args)
        {
            // вот тут должно происходить всё самое интересное
        }
    };
}

template<typename F>
detail::Dropper<F> DropArgs (const F& f)
{
	return detail::Dropper<F> { f };
}

Мы пока не знаем, как описать возвращаемый из operator() тип, поэтому просто оставим auto.

Что должно происходить в operator()? Пока что будем думать в терминах типов: массив Args по условию задачи делится на два подмассива InvokableArgs и Rest такие, что функтор F может быть вызван с аргументами с типами из InvokableArgs, ну а Rest — всё остальное. Например, для мотивирующего кода в начале статьи InvokableArgs пуст для обоих функторов, а Rest равен [const Media::IAudioSource*, QModelIndex] и [QModelIndex] соответственно (с точностью до CV-квалификации).

Обратим внимание, что типы, переданные в operator(), не обязаны полностью совпадать с типами, которые ожидает функтор (если таковые есть), преобразование типов тоже неплохо бы корректно поддерживать. Например если функтор принимает long, а в наш operator() передали int, то ничего страшного, InvokableArgs = [int].

Отметим, что если у функтора есть аргументы по умолчанию, то, вообще говоря, корректное разбиение Args на InvokableArgs и Rest не единственно: можно как передать их конкретные значения в функтор (и он будет вполне вызываем), так и проигнорировать их (при этом, собственно, подставятся аргументы по умолчанию):
auto func = [] (int n, double d, const std::string& s = "hello world", const std::vector<int>& v = {}) {};
auto dropped = DropArgs (func);

dropped (1, 3.14);
// в предыдущей строке у нас единственный вариант разбиения:
// InvokableArgs = [int, double], Rest = []

dropped (1, 3.14, "bye cruel world", { 1, 2, 3 });
// один вариант разбиения:
// InvokableArgs = [int, double, std::string, std::vector<int>], Rest = []
// func вызовется с параметрами n = 1, d = 3.14, s = "bye cruel world", v = { 1, 2, 3 }
//
// второй вариант разбиения:
// InvokableArgs = [int, double, std::string], Rest = [std::vector<int>]
// func вызовется с параметрами n = 1, d = 3.14, s = "bye cruel world", v = {}
//
// третий вариант разбиения:
// InvokableArgs = [int, double], Rest = [std::string, std::vector<int>]
// func вызовется с параметрами n = 1, d = 3.14, s = "hello world", v = {}

По очевидным причинам имеет смысл стремиться выбирать разбиения с максимальным InvokableArgs (первый вариант в примере выше).

Так вот, в operator() попробуем выяснить максимальный InvokableArgs для данного Args.

Как это можно сделать? Можно пытаться вызвать наш функтор F с полным списком Args. Получилось — хорошо, не получилось — откусываем тип с конца списка и пробуем ещё раз, и так далее, пока не придём к успеху (или пока типы не кончатся, но это будет означать, что InvokableArgs не существует для данного Args, что есть ошибка).

Напишем вспомогательный класс-хранитель типов (хотя и std::tuple бы сошёл, но хочется гарантированно избежать оверхеда, сделав класс пустым):
namespace detail
{
    template<typename...>
    struct Typelist {};
}
Единственная цель и смысл жизни этого класса — хранить конкретный список типов, с которым он инстанциирован.

Откусывать тип с конца списка типов эффективно за O(1) инстанциирований, насколько мне известно, невозможно, самое оптимальное — O(logn), да и то требует некоторых дополнительных трюков и манипуляций, посему оставляется читателю в качестве упражнения, а мы реализуем тупой лобовой O(n)-алгоритм: развернём список, откусим у него первый элемент и развернём ещё раз. И пусть компилятор пыхтит!

Функция откусывания первого элемента выглядит легко и просто, так что с неё и начнём, назвав её Tail в духе, приятном и понятном всем функциональщикам:
namespace detail
{
    template<template<typename...> class List, typename H, typename... T>
    constexpr List<T...> Tail (List<H, T...>)
    {
        return {};
    }
}
Обратим внимание, что возвращаемое значение, равно как и значение аргумента, нас совершеннейше не волнуют, имеют значение лишь их типы. Похожий паттерн будет преследовать нас на протяжении всей статьи. Кроме того, пожалуй, мы могли бы сразу договориться везде использовать конкретный Typelist вместо обобщённого аргумента шаблона List, но приведённый выше подход представляется более общным.

Для разворота списка нам сначала понадобится функция склейки двух списков типов:
namespace detail
{
    template<template<typename...> class List, typename... Args1, typename... Args2>
    constexpr List<Args1..., Args2...> Concat (List<Args1...>, List<Args2...>)
    {
        return {};
    }
}

Теперь можно и рекурсивно список развернуть, переложив на C++ следующий подозрительно похожий на Haskell псевдокод reverse [] = []; reverse (x:xs) = reverse xs ++ [x]:
namespace detail
{
    template<template<typename...> class List>
    constexpr List<> Reverse (List<>)
    {
    	return {};
    }

    template<template<typename...> class List, typename Head, typename... Tail>
    constexpr auto Reverse (List<Head, Tail...>) -> decltype (Concat (Reverse (List<Tail...> {}), List<Head> {}))
    {
        return {};
    }
}
Однако, тут не всё так гладко, если вдаваться в мелочи.
Почему?
Похоже, этот код не является корректным C++11-кодом. Точкой объявления функции в C++ при наличии trailing return type specifier является, собственно, окончание trailing return type specifier, поэтому внутри него самого ссылаться на функцию нельзя. Однако, по какой-то причине и gcc, и clang счастливо принимают этот код. Впрочем, мы чуть позже встретимся с похожим кодом, который gcc не принимает, и рассмотрим один из способов обойти эту проблему.

А в C++14 бы такой проблемы и не было, ибо мы могли бы вторую функцию переписать как
    template<template<typename...> class List, typename Head, typename... Tail>
    constexpr auto Reverse (List<Head, Tail...>)
    {
        return Concat (Reverse (List<Tail...> {}), List<Head> {});
    }
и не знать вообще никаких проблем: компилятор сам выведет корректный тип.

Отлично, теперь мы готовы, наконец, выяснить максимальный список типов, с которым наш искомый функтор всё ещё может быть вызван. Для этого напишем функцию GetInvokablePart(), которая, будучи инстанциированной нашим функтором и списком типов Args из Dropper::operator(), будет иметь возвращаемый тип Typelist<InvokableArgs>, инстанциированный соответствующими аргументами.
    template<typename F, template<typename...> class List, typename... Args>
    constexpr List<Args...> GetInvokablePartImpl (int, List<Args...>, typename std::result_of<F (Args...)>::type* = nullptr)
    {
        return {};
    }

    template<typename F, template<typename...> class List>
    constexpr Typelist<> GetInvokablePartImpl (float, List<>)
    {
        return {};
    }

    template<typename F, template<typename...> class List, typename... Args>
    constexpr auto GetInvokablePartImpl (float, List<Args...> list) ->
            decltype (GetInvokablePartImpl<F> (0, Reverse (Tail (Reverse (list)))))
    {
        return {};
    }

    template<typename F, typename... Args>
    constexpr auto GetInvokablePart () -> decltype (GetInvokablePartImpl<F> (0, Typelist<Args...> {}))
    {
        return {};
    }

Что здесь вообще происходит и как это работает? А здесь происходит вполне себе обычное SFINAE.

Начнём с функции GetInvokablePart(). Её возвращаемый тип равен возвращаемому типу функции GetInvokablePartImpl(), вызванной с данным списком типов (пока что полным Args). Как можно видеть, функций GetInvokablePartImpl() три штуки, и вывод вызываемого списка типов спрятан как раз в логике выбора каждой конкретной перегрузки. В этой логике мы опираемся на два факта (если сильно упрощать правила выбора перегруженных функций и специфицировать их для нашего конкретного случая):
  1. Если для вызова подходят две функции, в остальном одинаково равные, но у одной тип первого аргумента — int, а у второй — float, то при передаче значения 0 выберется перегрузка с int.
  2. Первая функция (с std::result_of) существует только тогда, когда существует тип result_of, а существует он только тогда, когда функтор F может быть вызван с аргументами типов Args. Иначе эта перегрузка убирается из рассмотрения.


Иными словами, если с текущим набором Args функтор вызываем, то выбирается первая перегрузка, и тип возвращаемого значения GetInvokablePart() равен Typelist<Args...>. Иначе выбирается третья перегрузка (если Args непуст, иначе — вторая, и там по-хорошему надо показывать человекочитаемое сообщение об ошибке, ибо мы все аргументы пооткусывали уже, а функция всё ещё не вызывается). Третья перегрузка откусывает последний элемент списка типов и пробует снова, а её возвращаемый тип, соответственно, равен возвращаемому типу рекурсивно вызванной функции с меньшим списком типов, возвращаемый тип которой равен… В общем, таким образом мы либо встретим, наконец, последовательность Args, с которой функтор можно вызвать, и тип функции будет соответствовать этому типу, либо упрёмся во вторую перегрузку, и это ошибка.

Итак, таким нехитрым образом рекурсивно можно вывести возвращаемый тип GetInvokablePart(). Добавим это в наш operator():
        template<typename... Args>
        auto operator() (Args&&... args)
        {
            auto invokableList = GetInvokablePart<F, Args...> ();
        }
Если мы теперь попробуем использовать DropArgs() для любого разбиения с непустым Rest, то уже сейчас увидим, что gcc не может корректно использовать GetInvokablePart(). Почему?

Как я уже писал в спойлере чуть выше, объявление
template<typename F, template<typename...> class List, typename... Args>
constexpr auto GetInvokablePartImpl (float, List<Args...> list) ->
        decltype (GetInvokablePartImpl<F> (0, Reverse (Tail (Reverse (list)))))
не очень корректно с точки зрения C++: точкой объявления функции при наличии trailing return type specifier является, собственно, окончание trailing return type specifier, поэтому внутри него самого ссылаться на функцию нельзя (есть пропозалы на тему того, чтобы это исправить и считать точкой объявления функции символ ->, но пока что имеем то, что имеем). Есть два способа это починить:

  1. Забить на C++11 и писать на C++14 без всяких trailing return type'ов,
    как-то так.
        template<typename F, template<typename...> class List, typename... Args>
        constexpr List<Args...> GetInvokablePartImpl (int, List<Args...>, typename std::result_of<F (Args...)>::type* = nullptr)
        {
            return {};
        }
    
        template<typename F, template<typename...> class List>
        constexpr Typelist<> GetInvokablePartImpl (float, List<>)
        {
            return {};
        }
    
        template<typename F, template<typename...> class List, typename... Args>
        constexpr auto GetInvokablePartImpl (float, List<Args...> list)
        {
            return GetInvokablePartImpl<F> (0, Reverse (Tail (Reverse (list))));
        }
    
        template<typename F, typename... Args>
        constexpr auto GetInvokablePart ()
        {
            return GetInvokablePartImpl<F> (0, Typelist<Args...> {});
        }
    
  2. Переписать третью перегрузку с костылями так, чтобы вынести ссылку функции на саму себя, «развязав» её с объявлением возвращаемого типа. Для этого добавим вспомогательную структуру, тип внутри которой будет равен типу соответствующей функции, а внутри самой функции будем ссылаться на эту структуру. Как-то так:
        template<typename F, typename List>
        struct InvokableType;
    
        template<typename F, template<typename...> class List, typename... Args>
        constexpr auto GetInvokablePartImpl (float, List<Args...> list) ->
                typename InvokableType<F, decltype (Reverse (Tail (Reverse (list))))>::RetType_t
        {
            return {};
        }
    
        template<typename F, typename List>
        struct InvokableType
        {
            using RetType_t = decltype (GetInvokablePartImpl<F> (0, List {}));
        };
    


Итак, что мы имеем на данный момент? У нас есть полный список аргументов Args, переданный в operator(), и некоторое его начало InvokableArgs. Что дальше? А дальше попробуем взять и передать этот список в некоторую вспомогательную функцию, которая и будет заниматься вызовом функции. Наивно напишем подобный код внутри класса Dropper:
        template<typename... Args>
        auto operator() (Args&&... args)
        {
            auto invokableList = GetInvokablePart<F, Args...> ();
            return Invoke (invokableList, std::forward<Args> (args)...);
        }
    private:
        template<typename... InvokableArgs, typename... Rest>
        auto Invoke (Typelist<InvokableArgs...>, InvokableArgs&&... args, Rest&&...)
        {
            return F_ (std::forward<InvokableArgs> (args)...);
        }
понадеявшись, что компилятор достаточно умный и выведет InvokableArgs из первого аргумента шаблона, а Rest — из остатка. Однако, наши надежды не оправдаются.

Что ж, попробуем указать Rest руками. Как это лучше всего сделать? На самом деле, достаточно просто откусить с головы столько элементов, сколько содержится в InvokableArgs. Для этого нам понадобится функция подсчёта числа элементов в списке типов:
    template<template<typename...> class List, typename... Args>
    constexpr size_t Length (List<Args...>)
    {
        return sizeof... (Args);
    }
и функция отбрасывания первых N типов в списке. Например:
    template<int N, typename List>
    struct DropImpl
    {
        using Result_t = typename DropImpl<N - 1, decltype (Tail (List {}))>::Result_t;
    };

    template<typename List>
    struct DropImpl<0, List>
    {
        using Result_t = List;
    };

    template<int N, template<typename...> class List, typename... Args>
    constexpr typename DropImpl<N, List<Args...>>::Result_t Drop (List<Args...>)
    {
        return {};
    }

Немного про альтернативы
Кстати, если писать Drop не через структуру, как здесь, а через рекурсивно вызываемые функции с постоянно уменьшающимся счётчиком, отбрасываемые по SFINAE при достижении счётчиком нуля, то gcc 4.9 уходит в очень забавный бесконечный цикл инстанциаций и не способен выйти из него самостоятельно. Похоже, проблема в том, что он инстанциирует возвращаемый тип функции до отбрасывания её по SFINAE в списке аргументов. Не уверен, впрочем, что это баг.


С учётом вышесказанного, модифицируем наш Dropper следующим образом:
        template<typename... Args>
        auto operator() (Args&&... args)
        {
            constexpr auto invokableList = GetInvokablePart<F, Args...> ();
            auto ignoreList = Drop<Length (invokableList)> (Typelist<Args...> {});
            return Invoke (invokableList, ignoreList, std::forward<Args> (args)...);
        }
    private:
        template<typename... InvokableArgs, typename... Rest>
        auto Invoke (Typelist<InvokableArgs...>, Typelist<Rest...>, InvokableArgs&&... args, Rest&&...)
        {
            return F_ (std::forward<InvokableArgs> (args)...);
        }
Не забываем constexpr у invokableList, иначе пришлось бы писать что-то вроде Drop<Length (decltype (invokableList) {})>.

Отлично! Кажется, это всё, работает!

На самом деле нет. gcc плюётся не очень информативной ошибкой:
prog.cc: In instantiation of 'void detail::Dropper<F>::operator()(Args&& ...) [with Args = {int&, double}; F = main()::<lambda(int)>]':
prog.cc:147:35:   required from here
prog.cc:125:67: error: no matching function for call to 'detail::Dropper<main()::<lambda(int)> >::Invoke(const detail::Typelist<int&>&, detail::Typelist<double>&, int&, double)'
    Invoke (invokableList, ignoreList, std::forward<Args> (args)...);
                                                                   ^
prog.cc:125:67: note: candidate is:
prog.cc:129:8: note: template<class ... InvokableArgs, class ... Rest> void detail::Dropper<F>::Invoke(detail::Typelist<InvokableArgs ...>, detail::Typelist<Rest ...>, InvokableArgs&& ..., Rest&& ...) [with InvokableArgs = {InvokableArgs ...}; Rest = {Rest ...}; F = main()::<lambda(int)>]
   void Invoke (Typelist<InvokableArgs...>, Typelist<Rest...>, InvokableArgs&&... args, Rest&&...)
        ^
prog.cc:129:8: note:   template argument deduction/substitution failed:
prog.cc:125:67: note:   inconsistent parameter pack deduction with '' and ''
    Invoke (invokableList, ignoreList, std::forward<Args> (args)...);
                                                                   ^


Однако, судя по почему-то кажущемуся очень смешным отрывку inconsistent parameter pack deduction with '' and '', gcc снесло голову при попытке сматчить InvokableArgs из первого аргумента (который Typelist), и из остатка списка аргументов. Что же делать?

Обычно к моей печали, но к локальному счастью в данном случае, в C++ ещё не завезли многоуровневый вывод типов вроде Хиндли-Милнера, поэтому в данном конкретном случае можно просто запретить компилятору выводить типы из третьего и последующих аргументов, для чего их надо обернуть в специальную тупую обёртку. Начнём с обёртки, которая не делает вообще ничего:
    template<typename T>
    struct Dumbifier
    {
        using Type_t = T;
    };
    
    template<typename T>
    using Dumbify = typename Dumbifier<T>::Type_t;
Тогда Dumbify<Args...> просто-напросто равно Args..., но компилятор не будет пытаться это выводить.

Осталось обновить Invoke:
    template<typename... InvokableArgs, typename... Rest>
    auto Invoke (Typelist<InvokableArgs...>, Typelist<Rest...>, Dumbify<InvokableArgs>... args, Dumbify<Rest>...)
    {
        return F_ (std::forward<InvokableArgs> (args)...);
    }


Для полной совместимости с C++11 осталось лишь вручную указать возвращаемые типы operator() и Invoke, это вместе с анализом perfect forwarding-качеств приведённого решения остаётся пытливому читателю в качестве упражнения.

Полный текст.
namespace detail
{
    template<typename... Args>
    struct Typelist
    {
    };

    template<template<typename...> class List, typename H, typename... T>
    constexpr List<T...> Tail (List<H, T...>)
    {
        return {};
    }

    template<int N, typename List>
    struct DropImpl
    {
        using Result_t = typename DropImpl<N - 1, decltype (Tail (List {}))>::Result_t;
    };

    template<typename List>
    struct DropImpl<0, List>
    {
        using Result_t = List;
    };

    template<int N, template<typename...> class List, typename... Args>
    constexpr typename DropImpl<N, List<Args...>>::Result_t Drop (List<Args...>)
    {
        return {};
    }

    template<template<typename...> class List, typename... Args1, typename... Args2>
    constexpr List<Args1..., Args2...> Concat (List<Args1...>, List<Args2...>)
    {
        return {};
    }

    template<template<typename...> class List>
    constexpr List<> Reverse (List<>)
    {
        return {};
    }

    template<template<typename...> class List, typename Head, typename... Tail>
    constexpr auto Reverse (List<Head, Tail...>) -> decltype (Concat (Reverse (List<Tail...> {}), List<Head> {}))
    {
        return {};
    }

    template<typename F, template<typename...> class List, typename... Args>
    constexpr List<Args...> GetInvokablePartImpl (int, List<Args...>, typename std::result_of<F (Args...)>::type* = nullptr)
    {
        return {};
    }

    template<typename F, template<typename...> class List>
    constexpr Typelist<> GetInvokablePartImpl (float, List<>)
    {
        return {};
    }

    template<typename F, typename List>
    struct InvokableType;

    template<typename F, template<typename...> class List, typename... Args>
    constexpr auto GetInvokablePartImpl (float, List<Args...> list) ->
                typename InvokableType<F, decltype (Reverse (Tail (Reverse (list))))>::RetType_t
    {
        return {};
    }

    template<typename F, typename List>
    struct InvokableType
    {
        using RetType_t = decltype (GetInvokablePartImpl<F> (0, List {}));
    };

    template<typename F, typename... Args>
    constexpr auto GetInvokablePart () -> decltype (GetInvokablePartImpl<F> (0, Typelist<Args...> {}))
    {
        return GetInvokablePartImpl<F> (0, Typelist<Args...> {});
    }

    template<template<typename...> class List, typename... Args>
    constexpr size_t Length (List<Args...>)
    {
        return sizeof... (Args);
    }
    
    template<typename T>
    struct Dumbifier
    {
        using Type_t = T;
    };
    
    template<typename T>
    using Dumbify = typename Dumbifier<T>::Type_t;

    template<typename F, typename List>
    struct InvokableResGetter;

    template<typename F, template<typename...> class List, typename... Args>
    struct InvokableResGetter<F, List<Args...>>
    {
        using RetType_t = typename std::result_of<F (Args...)>::type;
    };

    template<typename F>
    class Dropper
    {
        F F_;
    public:
        Dropper (const F& f)
        : F_ (f)
        {
        }

        template<typename... Args>
        auto operator() (Args... args) ->
                typename InvokableResGetter<F, decltype (GetInvokablePart<F, Args...> ())>::RetType_t
        {
            constexpr auto invokableList = GetInvokablePart<F, Args...> ();
            auto ignoreList = Drop<Length (invokableList)> (Typelist<Args...> {});
            return Invoke (invokableList, ignoreList, std::forward<Args> (args)...);
        }
    private:
        template<typename... InvokableArgs, typename... Rest>
        auto Invoke (Typelist<InvokableArgs...>, Typelist<Rest...>, Dumbify<InvokableArgs>... args, Dumbify<Rest>...) ->
                typename std::result_of<F (InvokableArgs...)>::type
        {
            return F_ (std::forward<InvokableArgs> (args)...);
        }
    };
}

template<typename F>
detail::Dropper<F> DropArgs (const F& f)
{
	return detail::Dropper<F> { f };
}



В качестве послесловия проведём небольшой анализ оверхеда решения.

Дизасм функции
int Bar ()
{
    return DropArgs ([] (int n) { return n * 2; }) (1, 2.5);
}
показывает, что все gcc 4.8+, похоже, достаточно умны, чтобы всю функцию свести к return 2, равно как и clang 3.3+. Молодцы. icc 13.0, к сожалению, код собрать не может.

Если же написать
int Bar ()
{
    volatile int n = 1;
    return DropArgs ([] (int n) { return n * 2; }) (n, 2.5);
}
то снова все компиляторы закономерно разворачивают это во что-то вроде
Bar():
	movl	$1, -4(%rsp)
	movl	-4(%rsp), %eax
	addl	%eax, %eax
	ret


Придумать сходу случай, когда возникал бы существенный оверхед, мне не удалось.

Комментарии (11)


  1. slonopotamus
    04.08.2015 19:53

    Безусловно, круто. Но как это сказывается на времени компиляции?


    1. TheRipper
      04.08.2015 20:01
      +6

      Скорее всего, вызывает рак у компиляторов.


    1. 0xd34df00d
      04.08.2015 20:02

      Хороший вопрос. На глазок и на тестовых примерах — особо никак.

      Надо подумать, как бы сделать бенчмарки времени компиляции.


      1. VioletGiraffe
        04.08.2015 20:33
        +2

        Если проект Visual Studio, то для Студии есть классный бесплатный аддон Build Monitor который показывает время сборки проектов / солюшена.


        1. 0xd34df00d
          04.08.2015 21:21

          А под *NIX с gcc и clang можно и просто time использовать.

          Вопрос скорее не в том, как засекать время, а в том, как правильно измерить только вот DropArgs() и всё связанное с ней. А то, например, если генерировать всевозможные функторы аналогично в компил-тайме тем или иным образом, можно случайно заодно измерить скорость и стоимость такого компилтайм-порождения.


          1. khim
            04.08.2015 23:08

            Там ещё и зависимость нифига не линейная. У нас был проектик, в котором файлик компилировался секунд 5 с помощью GCC и секунд 30 с помощью MSVC (2005й, если мне не изменяет память). А потом в файлике произошли изменения и GCC стал собирать его за минуту, а MSVC — не за 5-6 (как можно было ожидать), а за час. Но тут как раз подоспел перевод проекта на MSVC 2008 и, о счастье: компиляция занимает всего пару минут, круто! Мы почти возрадовались, но потом как-то решили проверить результат компиляции. И тут наша радость подумеркла: MSVC 2008 не нашёл ничего лучшего, как отключить оптимизацию! Вообще. Бенчмарки что с оптимизацией, что без работали одинаковое время!

            С тех пор прошло много лет, надо бы перепроверить, но, я не удивлюсь если всё останется как было. Впрочем это небольшая проблема: всегда можно собрать код, где скорость важна с помощью clang'а…


  1. vScherba
    05.08.2015 01:55
    +8

    Я пока не осилил всю статью, но с первого примера возник вопрос: а почему бы просто не использовать bind?

    const bool exists = WithObject (objectId,
            std::bind ([] { return true; }),
            std::bind ([] { return false; }));
    

    // вернет false
    // в MS VS 2012 в машинном коде в релизе нулевой оверхед: "push 0" - если результат передать аргументом другой функции
    std::bind([] { return false; })(0, 3.14, "foobar");
    


    1. 0xd34df00d
      05.08.2015 03:38
      +1

      Потому что это слишком скучно.


    1. xanep
      05.08.2015 08:01
      +8

      Потому что у разработчиков ПО есть древняя традиция велосипедостроения.


  1. Woodroof
    05.08.2015 18:33

    Со включенным -Wunused-parameter ругается?


    1. 0xd34df00d
      05.08.2015 18:47

      gcc? Да, он не считает упоминание параметра с trailing return type использованием.

      Можно переобъявить параметр с тем же типом, а можно в теле написать что-то вроде (void)list;