Неожиданный результат при попытке составить карри лямбда с другой лямбдой

Я играю с С++ 11 lambdas и пытаюсь имитировать некоторую функцию из functional module языка программирования D. Я действительно пытался реализовать curry и compose. Вот main, который я пытаюсь получить:

int main()
{
    auto add = [](int a, int b)
    {
        return a + b;
    };
    auto add5 = curry(add, 5);

    auto composed = compose(add5, add);
    // Expected result: 25
    std::cout << composed(5, 15) << std::endl;
}

Проблема в том, что я не получаю тот же результат от g++ и clang++. Я получаю:

  • 35 с g++ 4.8.1
  • 25 с g++ 4.8.2
  • 25 с g++ 4.9
  • 32787 с clang++ 3.5 (соединительная линия, используемая с Coliru)

g++ 4.8.2 и 4.9 дают ожидаемый результат. Результаты, полученные из g++ 4.8.1 и clang 3.5, не зависят от значения, переданного в curry. Сначала я подумал, что это может быть ошибка компилятора, но более вероятно, что у меня есть ошибка в моем коде.


Вот моя реализация curry:

template<typename Function, typename First, std::size_t... Ind>
auto curry_impl(const Function& func, First&& first, indices<Ind...>)
    -> std::function<
        typename function_traits<Function>::result_type(
        typename function_traits<Function>::template argument_type<Ind>...)>
{
    return [&](typename function_traits<Function>::template argument_type<Ind>&&... args)
    {
        return func(
            std::forward<First>(first),
            std::forward<typename function_traits<Function>::template argument_type<Ind>>(args)...
        );
    };
}

template<typename Function, typename First,
         typename Indices=indices_range<1, function_traits<Function>::arity>>
auto curry(Function&& func, First first)
    -> decltype(curry_impl(std::forward<Function>(func), std::forward<First>(first), Indices()))
{
    using FirstArg = typename function_traits<Function>::template argument_type<0>;
    static_assert(std::is_convertible<First, FirstArg>::value,
                  "the value to be tied should be convertible to the type of the function first parameter");
    return curry_impl(std::forward<Function>(func), std::forward<First>(first), Indices());
}

И вот моя реализация compose (обратите внимание, что я написал только двоичный compose, а D - переменный):

template<typename First, typename Second, std::size_t... Ind>
auto compose_impl(const First& first, const Second& second, indices<Ind...>)
    -> std::function<
        typename function_traits<First>::result_type(
        typename function_traits<Second>::template argument_type<Ind>...)>
{
    return [&](typename function_traits<Second>::template argument_type<Ind>&&... args)
    {
        return first(second(
            std::forward<typename function_traits<Second>::template argument_type<Ind>>(args)...
        ));
    };
}

template<typename First, typename Second,
         typename Indices=make_indices<function_traits<Second>::arity>>
auto compose(First&& first, Second&& second)
    -> decltype(compose_impl(std::forward<First>(first), std::forward<Second>(second), Indices()))
{
    static_assert(function_traits<First>::arity == 1u,
                  "all the functions passed to compose, except the last one, must take exactly one parameter");

    using Ret = typename function_traits<Second>::result_type;
    using FirstArg = typename function_traits<First>::template argument_type<0>;
    static_assert(std::is_convertible<Ret, FirstArg>::value,
                  "incompatible return types in compose");

    return compose_impl(std::forward<First>(first), std::forward<Second>(second), Indices());
}

Класс function_trait используется для получения arty, возвращаемого типа и типа аргументов лямбда. Этот код сильно зависит от трюка индексов. Поскольку я не использую С++ 14, я не использую std::index_sequence, а более старую реализацию под названием indices. indices_range<begin, end> - последовательность индексов, соответствующая диапазону [begin, end). Вы можете найти реализацию этих вспомогательных метафайлов (а также curry и compose) в онлайн-версии кода, но они менее значимы в этой проблеме.


У меня есть ошибка в реализации curry и/или compose или плохие результаты (с g++ 4.8.1 и clang++ 3.5) из-за ошибок компилятора?


РЕДАКТИРОВАТЬ: Вы можете найти код, который не совсем читается. Итак, вот версии curry и compose, которые являются точно такими же, но используют шаблоны псевдонимов, чтобы уменьшить шаблон. Я также удалил static_assert s; в то время как они могут быть полезной информацией, что слишком много текста для вопроса, и они не участвуют в этой проблеме.

template<typename Function, typename First, std::size_t... Ind>
auto curry_impl(const Function& func, First&& first, indices<Ind...>)
    -> std::function<
        result_type<Function>(
        argument_type<Function, Ind>...)>
{
    return [&](argument_type<Function, Ind>&&... args)
    {
        return func(
            std::forward<First>(first),
            std::forward<argument_type<Function, Ind>>(args)...
        );
    };
}

template<typename Function, typename First,
         typename Indices=indices_range<1, function_traits<Function>::arity>>
auto curry(Function&& func, First first)
    -> decltype(curry_impl(std::forward<Function>(func), std::forward<First>(first), Indices()))
{
    return curry_impl(std::forward<Function>(func), std::forward<First>(first), Indices());
}

template<typename First, typename Second, std::size_t... Ind>
auto compose_impl(const First& first, const Second& second, indices<Ind...>)
    -> std::function<
        typename result_type<First>(
        typename argument_type<Second, Ind>...)>
{
    return [&](argument_type<Second, Ind>&&... args)
    {
        return first(second(
            std::forward<argument_type<Second, Ind>>(args)...
        ));
    };
}

template<typename First, typename Second,
         typename Indices=make_indices<function_traits<Second>::arity>>
auto compose(First&& first, Second&& second)
    -> decltype(compose_impl(std::forward<First>(first), std::forward<Second>(second), Indices()))
{
    return compose_impl(std::forward<First>(first), std::forward<Second>(second), Indices());
}

Ответ 1

Как я считаю, другие упоминали в ваших комментариях, проблемы, связанные с вашим кодом, - это проблемы, связанные с жизнью. Обратите внимание, что вы передаете второй параметр, 5, в curry как rvalue:

auto add5 = curry(add, 5);

Затем при вызове функции curry вы создаете копию этой переменной в стеке как один из параметров:

auto curry(Function&& func, First first)

Затем в своем обращении к curry_impl вы передадите ссылку на first, которая не будет существовать после завершения вашего вызова на curry. Поскольку создаваемая вами лямбда использует ссылку на переменную, которая больше не существует, вы получаете поведение undefined.

Чтобы устранить проблему, которую вы испытываете, просто измените прототип curry, чтобы использовать универсальную ссылку на first и убедитесь, что вы не передаете rvalues ​​на curry:

template<typename Function, typename First,
         typename Indices=indices_range<1, function_traits<Function>::arity>>
auto curry(Function&& func, First&& first)
    -> decltype(curry_impl(std::forward<Function>(func), std::forward<First>(first), Indices()))
{
    using FirstArg = typename function_traits<Function>::template argument_type<0>;
    static_assert(std::is_convertible<First, FirstArg>::value,
                  "the value to be tied should be convertible to the type of the function first parameter");
    return curry_impl(std::forward<Function>(func), std::forward<First>(first), Indices());
}

Затем в основном:

int foo = 5;
auto add5 = curry(add, foo);

Конечно, ограничение на выражение lvalue - довольно большая проблема с интерфейсом, поэтому стоит упомянуть, что если вы планируете использовать это вне упражнений, было бы неплохо предоставить интерфейс, в котором могут быть rvalues б.

Затем снова я изменил бы его так, чтобы полученный функтор имел копии его компонентов как std::bind. Я знаю, что был бы немного озадачен, если следующий код не сработал:

std::function<int(int)> foo()
{
    std::function<int(int, int)> add = [](int a, int b)
    {
        return a + b;
    };
    return curry(add, 5);
}

Изменить: теперь я вижу, что некоторые версии gcc по-прежнему требуют, чтобы значения были записаны по значению в полученную lamba. GCC 4.9.0 20131229 - это сборка, в которой я тестировал ее, на которой отлично работает.

Изменить # 2: указать правильное использование за Xeo