Тип совпадения унаследованных функций-членов

У меня есть следующий фрагмент кода, который не компилируется.

#include <iostream>

struct A {
    void foo() {}
};

struct B : public A {
    using A::foo;
};

template<typename U, U> struct helper{};

int main() {
    helper<void (A::*)(), &A::foo> compiles;
    helper<void (B::*)(), &B::foo> does_not_compile;

    return 0;
}

Он не компилируется, так как &B::foo преобразуется в &A::foo и поэтому не может соответствовать предложенному типу void (B::*)(). Поскольку это часть шаблона SFINAE, который я использую для проверки очень специфического интерфейса (я задаю определенные типы аргументов и типы вывода), я хотел бы, чтобы это работало независимо от наследования, сохраняя при этом проверку читабельной.

То, что я пробовал, включает в себя:

  • Кастинг второй части аргумента:

    helper<void (B::*)(), (void (B::*)())&B::foo> does_not_compile;

    К сожалению, это не помогает, так как вторая часть теперь не распознается как постоянное выражение и не работает.

  • Я попытался присвоить ссылку переменной, чтобы проверить это.

    constexpr void (B::* p)() = &B::foo; helper<void (B::* const)(), p> half_compiles;

    Этот код принимает clang 3.4, но g++ 4.8.1 отвергает его, и я понятия не имею, кто прав.

Есть идеи?

РЕДАКТИРОВАТЬ: Поскольку во многих комментариях запрашивается более конкретная версия проблемы, я напишу ее здесь:

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

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


person Svalorzen    schedule 06.05.2014    source источник
comment
Не могли бы вы предоставить более подробную информацию о том, чего вы пытаетесь достичь? Вероятно, есть альтернативный способ сделать это.   -  person jrok    schedule 06.05.2014
comment
Поскольку decltype уже упоминалось, могу я предложить вам написать helper<decltype(&B::foo), &B::foo> compiles; ?   -  person Ivan Vergiliev    schedule 06.05.2014
comment
Конечная цель следующая: ideone.com/mxIVw3 (я могу скопировать сюда, если вы считаете, что это стоит , я думаю, что это умаляет). Идея, как я уже сказал, состоит в том, чтобы как можно точнее проверить публичные интерфейсы, сохраняя при этом удобочитаемость.   -  person Svalorzen    schedule 06.05.2014
comment
@IvanVergiliev Проблема в том, что будет соответствовать foo независимо от его фактической подписи, в то время как я хочу убедиться, что он соответствует тому, что мне нужно.   -  person Svalorzen    schedule 06.05.2014
comment
@svalorzen <signature, decltype, pointer, convertable_magic>? С магией в качестве параметра =void (или чего-то подобного).   -  person Yakk - Adam Nevraumont    schedule 06.05.2014
comment
@Svalorzen: Какова твоя цель? Если вы просто хотите проверить наличие интерфейса для заданного типа T, то есть лучшие способы сделать это.   -  person Nawaz    schedule 06.05.2014
comment
@Yakk Не могли бы вы привести более конкретный пример? На самом деле я не настолько эксперт; хотя я точно могу работать и строить на примере =)   -  person Svalorzen    schedule 06.05.2014
comment
@ Наваз, правда, я бы не знал. То, что я делаю, я делаю после бесконечного просмотра ответов и блогов StackOverflow и выбора фрагментов, которые кажутся наиболее чистыми. Я определенно открыт для альтернатив, хотя это лучшее, что я мог придумать, это дает мне контроль, который я хочу.   -  person Svalorzen    schedule 06.05.2014
comment
@Svalorzen опишите вашу основную проблему. Вы хотите проверить, есть ли у типа X метод foo(), который можно вызывать с нулевыми аргументами и который возвращает что-то совместимое с R?   -  person Yakk - Adam Nevraumont    schedule 06.05.2014
comment
Хотя вам это не поможет: на самом деле вам нужны концепции и диспетчеризация на основе концепций.   -  person pmr    schedule 08.05.2014


Ответы (4)


Вот простой класс, который проходит ваши тесты (и не требует дюжины специализаций :)). Это также работает, когда foo перегружен. Подпись, которую вы хотите проверить, также может быть параметром шаблона (это хорошо, правда?).

#include <type_traits>

template <typename T>
struct is_foo {
    template<typename U>
    static auto check(int) ->
    decltype( static_cast< void (U::*)() const >(&U::foo), std::true_type() );
    //                     ^^^^^^^^^^^^^^^^^^^
    //                     the desired signature goes here

    template<typename>
    static std::false_type check(...);

    static constexpr bool value = decltype(check<T>(0))::value;
};

Живой пример здесь.

ИЗМЕНИТЬ:

У нас есть две перегрузки check. Оба могут принимать целочисленный литерал в качестве параметра, и, поскольку второй имеет многоточие в списке параметров, он никогда не будет лучшим вариантом разрешения перегрузки, когда обе перегрузки жизнеспособны (последовательность-преобразования-эллипса является хуже любой другой последовательности преобразования). Это позволяет нам однозначно инициализировать член value класса признаков позже.

Вторая перегрузка выбирается только тогда, когда первая отбрасывается из набора перегрузок. Это происходит, когда подстановка аргумента шаблона не удалась и не является ошибкой (SFINAE).

Это причудливое выражение слева от оператора запятой внутри decltype делает это возможным. Он может быть плохо сформирован, когда

  1. подвыражение &U::foo неправильно сформировано, что может произойти, когда

    • U is not a class type, or
    • U::foo недоступен или
    • нет U::foo
  2. результирующий указатель члена не может быть static_cast к целевому типу

Обратите внимание, что поиск &U::foo не завершается ошибкой, если сам U::foo будет неоднозначным. Это гарантируется в определенном контексте, указанном в стандарте C++ в разделе 13.4 (Адрес перегруженной функции, [over.over]). Одним из таких контекстов является явное преобразование типов (в данном случае static_cast).

Выражение также использует тот факт, что T B::* можно преобразовать в T D::*, где D — это класс, производный от B (но не наоборот). Таким образом, нет необходимости выводить тип класса, как в ответе iavr.

Член value затем инициализируется value из true_type или false_type.


Однако есть потенциальная проблема с этим решением. Рассмотреть возможность:

struct X {
    void foo() const;
};

struct Y : X {
    int foo();   // hides X::foo
};

Теперь is_foo<Y>::value даст false, потому что поиск имени для foo остановится, когда встретится Y::foo. Если это нежелательное поведение, рассмотрите возможность передачи класса, в котором вы хотите выполнить поиск, в качестве параметра шаблона is_foo и используйте его вместо &U::foo.

Надеюсь, это поможет.

person jrok    schedule 07.05.2014
comment
Подожди, ты, я, что. Мне нужно поиграть с этим, я выйду за тебя замуж! - person Svalorzen; 08.05.2014
comment
Это прекрасно работает, и это ТОЧНО, как я хотел, и я не могу достаточно проголосовать за это. Прежде чем принять это, не могли бы вы дать некоторое представление о том, как вы это получили? Там так мало информации о том, как работать с шаблонами, было бы здорово получить информацию о том, как это сделать! - person Svalorzen; 08.05.2014
comment
@Svalorzen Боюсь, я уже женат. Моя жена не возражает против редактирования ТАК ответов, так что вот. :) Дайте мне знать, если что-то неясно. - person jrok; 08.05.2014
comment
+1 Очень аккуратно. Кроме того, работает для перегруженных функций! Надеюсь, вы хотя бы видите, что моя дюжина специализаций — это повторно используемые компоненты — я написал их задолго до того, как возник этот вопрос :-) - person iavr; 09.05.2014

Ниже приведено рабочее решение вашей проблемы, опубликованное на странице https://ideone.com/mxIVw3 — см. также живой пример.

Эта проблема в некотором смысле является продолжением вывести родительский класс унаследованного метода в C++. В моем ответе я определил черту типа member_class, которая извлекает класс из заданного указателя на тип функции-члена. Ниже мы используем еще несколько признаков для анализа и последующего синтеза такого типа.

Во-первых, member_type извлекает подпись, например. void (C::*)() дает void():

template <typename M> struct member_type_t { };
template <typename M> using  member_type = typename member_type_t <M>::type;

template <typename T, typename C>
struct member_type_t <T C::*> { using type = T;};

Затем member_class извлекает класс, например. void (C::*)() дает C:

template<typename>
struct member_class_t;

template<typename M>
using member_class = typename member_class_t <M>::type;

template<typename R, typename C, typename... A>
struct member_class_t <R(C::*)(A...)> { using type = C; };

template<typename R, typename C, typename... A>
struct member_class_t <R(C::*)(A...) const> { using type = C const; };

// ...other qualifier specializations

Наконец, member_ptr синтезирует указатель на тип функции-члена с учетом класса и подписи, например. C + void() дать void (C::*)():

template <typename C, typename S>
struct member_ptr_t;

template <typename C, typename S>
using member_ptr = typename member_ptr_t <C, S>::type;

template <typename C, typename R, typename ...A>
struct member_ptr_t <C, R(A...)> { using type = R (C::*)(A...); };

template <typename C, typename R, typename ...A>
struct member_ptr_t <C const, R(A...)> { using type = R (C::*)(A...) const; };

// ...other qualifier specializations

Две предыдущие черты нуждаются в большей специализации, чтобы разные квалификаторы были более общими, например. const/volatile или ref-квалификаторы. Есть 12 комбинаций (или 13, включая элементы данных); полная реализация находится здесь< /а>.

Идея состоит в том, что любые квалификаторы переносятся с помощью member_class из типа указателя на функцию-член в сам класс. Затем member_ptr переносит квалификаторы из класса обратно в тип указателя. Хотя квалификаторы находятся в типе класса, можно свободно манипулировать стандартными признаками, например. добавить или удалить const, ссылки lvalue/rvalue и т. д.

А теперь вот ваш is_foo тест:

template <typename T>
struct is_foo {
private:
    template<
        typename Z,
        typename M = decltype(&Z::foo),
        typename C = typename std::decay<member_class<M>>::type,
        typename S = member_type<M>
    >
    using pattern = member_ptr<C const, void()>;

    template<typename U, U> struct helper{};

    template <typename Z> static auto test(Z z) -> decltype(
        helper<pattern<Z>, &Z::foo>(),
        // All other requirements follow..
        std::true_type()
    );

    template <typename> static auto test(...) -> std::false_type;

public:
    enum { value = std::is_same<decltype(test<T>(std::declval<T>())),std::true_type>::value };
};

Учитывая тип Z, шаблон псевдонима pattern получает правильный тип M указателя-члена с decltype(&Z::foo), извлекает его decayed класс C и сигнатуру S и синтезирует новый тип указателя на функцию-член с классом C const и сигнатурой void(), т.е. void (C::*)() const. Это именно то, что вам нужно: то же самое с вашим исходным жестко закодированным шаблоном, где тип Z заменен правильным классом C (возможно, базовым классом), найденным decltype.

Графически:

M = void (Z::*)() const  ->   Z         +   void()
                         ->   Z const   +   void()
                         ->   void (Z::*)() const   ==   M
                         ->   SUCCESS

M = int (Z::*)() const&  ->   Z const&   +   int()
                         ->   Z const    +   void()
                         ->   void (Z::*)() const   !=   M
                         ->   FAILURE

На самом деле подпись S здесь была не нужна, так что и member_type тоже. Но я использовал его в процессе, поэтому я включаю его сюда для полноты картины. Это может быть полезно в более общих случаях.

Конечно, все это не будет работать при множественных перегрузках, потому что decltype в данном случае не работает.

person iavr    schedule 06.05.2014
comment
Я вижу, что ты здесь делаешь. По сути, вы заставляете шаблоны разбивать все на составляющие и сами собираете спички. Это то, чего я надеялся избежать, так как я всегда находил эти стены шаблонов непроницаемыми. Тем не менее, большое спасибо за старания! - person Svalorzen; 06.05.2014
comment
@Svalorzen Если ваша цель — как можно точнее проверить общедоступные интерфейсы, я думаю, что это наиболее общее решение. Все трейты, определенные здесь, явно можно использовать повторно, они являются библиотечными компонентами. Вы можете использовать их почти так же, как указано в предоставленной ссылке, так что нечего вникать. На самом деле я обнаружил такие черты в реализациях std::result_of; они могли бы быть частью STL, но, к сожалению, это не так. Единственная часть, имеющая отношение к вашей проблеме, это псевдоним pattern. - person iavr; 06.05.2014
comment
@Svalorzen Кстати, спичек для сборки нет; только один тип. Данный тип разбивается на две части, с частями можно манипулировать/преобразовывать, а затем они снова объединяются в один тип. Здесь мы оставляем только одну часть (преобразованную в const) и заменяем другую часть на void(). - person iavr; 06.05.2014

Если вы просто хотите проверить наличие интерфейса для данного типа T, то есть лучшие способы сделать это. Вот один пример:

template<typename T>
struct has_foo
{
    template<typename U>
    constexpr static auto sfinae(U *obj) -> decltype(obj->foo(), bool()) { return true; }

    constexpr static auto sfinae(...) -> bool { return false; }

    constexpr static bool value = sfinae(static_cast<T*>(0));
};

Тестовый код:

struct A {
    void foo() {}
};

struct B : public A {
    using A::foo;
};

struct C{};

int main() 
{
    std::cout << has_foo<A>::value << std::endl;
    std::cout << has_foo<B>::value << std::endl;
    std::cout << has_foo<C>::value << std::endl;
    std::cout << has_foo<int>::value << std::endl;
    return 0;
}

Вывод (демонстрация):

1
1
0
0

Надеюсь, это поможет.

person Nawaz    schedule 06.05.2014
comment
Проблема в том, что это позволяет практически любой тип функции foo. Он не проверяет константность или виртуальный, он допускает любой возвращаемый тип и позволяет преобразовывать аргументы (целое число в беззнаковое и подобное). Вот почему я пытался избежать этого. - person Svalorzen; 06.05.2014
comment
@Svalorzen: Хорошо. затем посмотрите это: coliru.stacked-crooked.com/a/6f295c3345fb57ca ... Это нормально для вас? Конечно, он проверяет только тип параметра, а не тип возвращаемого значения (что можно сделать довольно легко). Он печатает 0 (false) для типов классов C и D, foo которых требует преобразования! - person Nawaz; 06.05.2014
comment
Это очень ловкий трюк! Не могли бы вы сделать так, чтобы он также проверял константность? - person Svalorzen; 06.05.2014
comment
@Svalorzen: constness чего? параметр? или функция? - person Nawaz; 06.05.2014
comment
@Nawaz Полагаю, из функции. Например. указатель функции-члена должен иметь тип void (Z::*)() const. - person iavr; 06.05.2014
comment
@Svalorzen: обратите внимание, что constness функции можно легко проверить, используя obj как T * const. Но константность параметров верхнего уровня не может быть проверена, потому что компилятор игнорирует их при интерпретации сигнатуры функции (т.е. void f(int); и void f(const int); - это одно и то же объявление! - person Nawaz; 06.05.2014
comment
@Nawaz Однако использование obj в качестве const заставляет каждую требуемую функцию быть const, хотя это может не обязательно быть так (нам могут понадобиться определенные foo() const и bar(), которые не обязательно должны быть const). - person Svalorzen; 06.05.2014
comment
@Svalorzenat: недостаточно ясно. Что ты хочешь? Вы хотите проверить, является ли foo константой или нет? Проверка на const не делает другую функцию bar автоматически константной. - person Nawaz; 06.05.2014

Я предлагаю использовать decltype для общего определения типа указателей на функции-члены:

helper<decltype(&A::foo), &A::foo> compiles;
helper<decltype(&B::foo), &B::foo> also_compiles;

Это может показаться нарушением DRY, но повторять имя принципиально не хуже, чем указывать тип отдельно от имени.

person Casey    schedule 06.05.2014
comment
Проблема в том, что вы проверяете наличие члена с именем foo, но не можете обеспечить какой-либо общий интерфейс для метода. A::foo может быть void(), а B::foo может быть int(unsigned,double,float). Это действительно не может помочь, если вы проверяете интерфейс, потому что хотите использовать его в шаблонном алгоритме. - person Svalorzen; 06.05.2014
comment
Он только проверяет существование члена. Он даже не проверяет, является ли член функцией или данными. struct C { std::string foo; }; (неправильно) пройдет тест! - person Nawaz; 06.05.2014
comment
Это использование decltype было моей самой первой мыслью, и на самом деле было упомянуто в комментарии под вопросом Ивана Вергилева. Но я думаю, что сам вопрос был недостаточно ясен; Я понял настоящую проблему, только когда увидел код на https://ideone.com/mxIVw3. - person iavr; 06.05.2014
comment
@Svalorzen Это не проверяет что-либо, само по себе - это самое простое возможное решение проблемы в вопросе: передача типа, соответствующего именованному указателю на функцию-член. Если ваша реальная проблема не та, что описана в вопросе, вам следует задать другой вопрос или уточнить этот. - person Casey; 06.05.2014