Специализация шаблона на лямбда в С++ 0x

Я написал класс признаков, который позволяет мне извлекать информацию о аргументах и ​​типе функции или объекта функции в С++ 0x (проверяется с помощью gcc 4.5.0). Общий случай обрабатывает объекты функции:

template <typename F>
struct function_traits {
    template <typename R, typename... A>
    struct _internal { };

    template <typename R, typename... A>
    struct _internal<R (F::*)(A...)> {
        // ...
    };

    typedef typename _internal<decltype(&F::operator())>::<<nested types go here>>;
};

Затем у меня есть специализация для простых функций в глобальной области:

template <typename R, typename... A>
struct function_traits<R (*)(A...)> {
    // ...
};

Это отлично работает, я могу передать функцию в шаблон или объект функции, и он работает правильно:

template <typename F>
void foo(F f) {
    typename function_traits<F>::whatever ...;
}

int f(int x) { ... }
foo(f);

Что делать, если вместо передачи функции или объекта функции в foo, я хочу передать лямбда-выражение?

foo([](int x) { ... });

Проблема заключается в том, что не применяется ни специализация function_traits<>. В проекте С++ 0x говорится, что тип выражения является "уникальным, неназванным, неединичным типом класса". Погрешность результата вызова typeid(...).name() в выражении дает мне то, что кажется gcc внутренним соглашением именования для лямбда, main::{lambda(int)#1}, а не то, что синтаксически представляет собой имя файла С++.

Короче, есть ли что-нибудь, что я могу внести в шаблон здесь:

template <typename R, typename... A>
struct function_traits<????> { ... }

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

Ответ 1

Я думаю, что можно специализировать черты для лямбда и сопоставлять шаблоны подписи неназванного функтора. Вот код, который работает на g++ 4.5. Хотя это работает, сопоставление образцов на лямбда, похоже, работает вопреки интуиции. У меня есть встроенные комментарии.

struct X
{
  float operator () (float i) { return i*2; }
  // If the following is enabled, program fails to compile
  // mostly because of ambiguity reasons.
  //double operator () (float i, double d) { return d*f; } 
};

template <typename T>
struct function_traits // matches when T=X or T=lambda
// As expected, lambda creates a "unique, unnamed, non-union class type" 
// so it matches here
{
  // Here is what you are looking for. The type of the member operator()
  // of the lambda is taken and mapped again on function_traits.
  typedef typename function_traits<decltype(&T::operator())>::return_type return_type;
};

// matches for X::operator() but not of lambda::operator()
template <typename R, typename C, typename... A>
struct function_traits<R (C::*)(A...)> 
{
  typedef R return_type;
};

// I initially thought the above defined member function specialization of 
// the trait will match lambdas::operator() because a lambda is a functor.
// It does not, however. Instead, it matches the one below.
// I wonder why? implementation defined?
template <typename R, typename... A>
struct function_traits<R (*)(A...)> // matches for lambda::operator() 
{
  typedef R return_type;
};

template <typename F>
typename function_traits<F>::return_type
foo(F f)
{
  return f(10);
}

template <typename F>
typename function_traits<F>::return_type
bar(F f)
{
  return f(5.0f, 100, 0.34);
}

int f(int x) { return x + x;  }

int main(void)
{
  foo(f);
  foo(X());
  bar([](float f, int l, double d){ return f+l+d; });
}

Ответ 2

Тройка void_t может помочь. Как работает` void_t`?

Если у вас нет С++ 17, вам нужно включить определение void_t:

template<typename... Ts> struct make_void { typedef void type;};
template<typename... Ts> using void_t = typename make_void<Ts...>::type;

Добавьте дополнительный шаблонный шаблон к исходному шаблону, по умолчанию - void:

template <typename T, typename = void>
struct function_traits;

Объект объектов для простых функций такой же, как и у вас:

template <typename R, typename... A>
struct function_traits<R (*)(A...)>
{
    using return_type = R;
    using class_type  = void;
    using args_type   = std:: tuple< A... >;
};

Для неконстантных методов:

template <typename R, typename... A>
struct function_traits<R (*)(A...)>
{
    using return_type = R;
    using class_type  = void;
    using args_type   = std:: tuple< A... >;
};

Не забудьте const методы:

template <typename R, typename C, typename... A>
struct function_traits<R (C::*)(A...) const> // const
{
    using return_type = R;
    using class_type  = C;
    using args_type   = std:: tuple< A... >;
};

Наконец, важная черта. Учитывая тип класса, включая типы лямбда, мы хотим перейти от T в decltype(&T::operator()). Мы хотим убедиться, что этот признак доступен только для типов T, для которых ::operator() доступен, и это то, что void_t для нас. Чтобы принудительно применить это ограничение, нам нужно помещать &T::operator() в подпись признаков где-то, следовательно template <typename T> struct function_traits<T, void_t< decltype(&T::operator())

template <typename T>
struct   function_traits<T, void_t< decltype(&T::operator()) > > 
: public function_traits<           decltype(&T::operator())   >
{
};

Метод operator() в (non mutable, non-generic) lambdas равен const, что объясняет, почему нам нужен шаблон const выше.

Но в конечном итоге это очень ограничительно. Это не будет работать с общими лямбдами или объектами с шаблоном operator(). Если вы передумаете свой дизайн, вы найдете более гибкий подход, более гибкий.

Ответ 3

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

Во-первых, я должен сказать, что соответствующий метод - это метод const, для лямбда (для не-захватывающего, не общего, не mutable лямбда). Таким образом, вы не сможете отличить истинную лямбду от этого:

struct {
    int operator() (int) const { return 7; }
} object_of_unnamed_name_and_with_suitable_method;

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



// First, a convenient struct in which to store all the results:
template<bool is_method_, bool is_const_method_, typename C, typename R, typename ...Args>
struct function_traits_results {
    constexpr static bool is_method = is_method_;
    constexpr static bool is_const_method = is_const_method_;
    typedef C class_type; // void for plain functions. Otherwise,
                          // the functor/lambda type
    typedef R return_type;
    typedef tuple<Args...> args_type_as_tuple;
};

// This will extract all the details from a method-signature:
template<typename>
struct intermediate_step;
template<typename R, typename C, typename ...Args>
struct intermediate_step<R (C::*) (Args...)>  // non-const methods
    : public function_traits_results<true, false, C, R, Args...>
{
};
template<typename R, typename C, typename ...Args>
struct intermediate_step<R (C::*) (Args...) const> // const methods
    : public function_traits_results<true, true, C, R, Args...>
{
};


// These next two overloads do the initial task of separating
// plain function pointers for functors with ::operator()
template<typename R, typename ...Args>
function_traits_results<false, false, void, R, Args...>
function_traits_helper(R (*) (Args...) );
template<typename F, typename ..., typename MemberType = decltype(&F::operator()) >
intermediate_step<MemberType>
function_traits_helper(F);


// Finally, the actual `function_traits` struct, that delegates
// everything to the helper
template <typename T>
struct function_traits : public decltype(function_traits_helper( declval<T>() ) )
{
};