Предоставляет ли функция SFINAEd-out явно импортированную перегрузку из базового класса

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

template<typename T>
struct wrapper: T
{
    using T::foo;

    template<typename Arg>
    auto foo(Arg) const
        -> std::enable_if_t<not std::is_constructible<Arg>::value, bool>
    {
        return false;
    }
};

struct bar
{
    template<typename Arg>
    auto foo(Arg) const
        -> bool
    {
        return true;
    }
};

В этом простом примере wrapper добавляет перегруженный foo только в том случае, если один из базового класса не является жизнеспособным (я упростил std::enable_if к простейшей возможной вещи, в оригинальной - идиоме обнаружения). Однако g++ и clang++ не согласны. Возьмите следующий main:

int main()
{
    assert(wrapper<bar>{}.foo(0));
}

g++ в порядке: foo from wrapper<bar> является SFINAEd, поэтому вместо него он использует один из bar. С другой стороны, clang++, похоже, предполагает, что wrapper<bar>::foo всегда тени bar::foo, даже когда SFINAEd выйдет. Вот сообщение об ошибке:

main.cpp:30:26: error: no matching member function for call to 'foo'
    assert(wrapper<bar>{}.foo(0));
       ~~~~~~~~~~~~~~~^~~

/usr/include/assert.h:92:5: note: expanded from macro 'assert'
  ((expr)                                                               \
        ^

/usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/5.2.0/../../../../include/c++/5.2.0/type_traits:2388:44: note: candidate template ignored: disabled by 'enable_if' [with Arg = int]
    using enable_if_t = typename enable_if<_Cond, _Tp>::type;
                                               ^

1 error generated.

Итак, кто прав? Должен ли этот код отклоняться так же, как и clang++, или он должен работать и вызывать bar::foo?

Ответ 1

Рассмотрим §10.2:

В наборе объявлений using-declarations заменяются набором назначенные члены, которые не являются скрытыми или переопределены членами (7.3.3),

И в §7.3.3 идет

Когда декларация использования приносит имена из базового класса в область производного класса, [...] шаблоны функций-членов в переопределении производного класса и/или скрывают функции-члены и шаблоны-члены-члены с тем же именем, параметр -type-list (8.3.5 [dcl.fct]), cv-qualification и ref-qualifier (если есть) в базовом классе (а не в конфликте).

Ясно, что единственная разница в вашем примере лежит в пределах типов возврата. Таким образом, Clang является правильным и GCC прослушивается.

Формулировка была введена CWG # 1764:

Согласно пункту 7.3.3 [namespace.udecl],

Когда декларация using приносит имена из базового класса в область производного класса, [...]

Алгоритм поиска имени класса-класса, приведенный в 10.2 [class.member.lookup], однако, не выполняет этого требования; нет ничего, что удаляет скрытый элемент базового класса (заменяя использование-декларация, согласно абзацу 3) из набора результатов.

Разрешение было перенесено на DR в феврале 2014 года, поэтому, возможно, GCC еще не реализовал его.


Как упоминалось в ответе @TartanLlama, вы можете ввести аналог для обработки другого случая. Что-то вдоль линий

template <typename Arg, typename=std::enable_if_t<std::is_constructible<Arg>{}>>
decltype(auto) foo(Arg a) const
{
    return T::foo(a);
}

Демо.

Ответ 2

Кажется, это ошибка; с явным использованием объявления, Clang должен учитывать базовую перегрузку. Нет причин для этого не делать.

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

Ответ 3

Как говорит @SebastianRedl, это похоже на ошибку clang (Edit: похоже, что мы были неверно).

Независимо от того, какой компилятор верен, возможным решением было бы определить две версии wrapper<T>::foo: один, когда T::foo не существует, что обеспечивает пользовательскую реализацию; и один, когда он это делает, который переадресует вызов базовой версии:

template<typename T>
struct wrapper: T
{
    template<typename Arg, typename U=T>
    auto foo(Arg) const
        -> std::enable_if_t<!has_foo<U>::value, bool>
    {
        return false;
    }

    template<typename Arg, typename U=T>
    auto foo(Arg a) const
        -> std::enable_if_t<has_foo<U>::value, bool>
    {
        return T::foo(a); //probably want to perfect forward a
    }
};

Live Demo