`enable_if` с проблемой` enum` специализации шаблона - PullRequest
0 голосов
/ 28 февраля 2019

У меня проблемы с компиляцией GCC enable_if s, применяемых для возврата значения метода шаблонного класса.С помощью Clang я могу использовать выражение в enable_if для аргумента шаблона enum, в то время как GCC отказывается компилировать этот код.

Вот описание проблемы, исходный код и его последующие модификации, которыепопробуйте удовлетворить меня и компиляторы (к сожалению, не одновременно).

У меня есть не шаблонный класс Logic, который содержит шаблонный метод класса computeThings(), который имеет enum Strategy как one его шаблонных параметров.Логика в computeThings() зависит от времени компиляции Strategy, поэтому if constexpr - разумный способ реализации.

Вариант 1

    #include <iostream>
class Logic {
public:
    enum Strategy { strat_A, strat_B };
    // class A and class B are dummy in this example, provided to show that there are several template
    // parameters, and strategy selection effectively results in 
    // partial (not full) templated method specification
    template <class A, class B, Strategy strategy>
    int computeThings();
};

template <class A, class B, Logic::Strategy strategy>
int Logic::computeThings() {
    if constexpr(strategy==strat_A)
        return 0;
    else
        return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

Вариант 1 работает нормальнои компилирует как в clang, так и в gcc.Однако я хочу избавиться от if constexpr и разделить computeThings() на два специализированных метода, основанных на выбранном Strategy.Причина: функция критична к производительности и содержит много кода.

Итак, я придумаю вариант 2, который использует enable_if, примененный к возвращаемому значению.

Вариант 2

#include <iostream>
class Logic {
public:
    enum Strategy { strat_A, strat_B };

    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<strategy==Logic::strat_A,int>
    computeThings();

    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<strategy==Logic::strat_B,int>
    computeThings();
};

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_A,int>
Logic::computeThings() {
    return 0;
}

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_B,int>
Logic::computeThings() {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

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

error: prototype for 'std::enable_if_t<(strategy == Logic:: strat_A),int> Logic::computeThings()' does not match any in class 'Logic' Logic::computeThings()

candidates are: template<class A, class B, Logic::Strategy strategy> std::enable_if_t<(strategy == strat_B), int> Logic::computeThings() computeThings();

candidates are: template<class A, class B, Logic::Strategy strategy> std::enable_if_t<(strategy == strat_A), int> Logic::computeThings() computeThings();

Так что, по-видимому, использование простого strategy==Logic::strat_A конфликтует с GCC.Итак, я пришел к решению, которое удовлетворяет как clang, так и gcc, которое включает strategy==Logic::strat_A в struct:

Вариант 3

#include <iostream>
class Logic {
public:
    enum Strategy { strat_A, strat_B };

    template <Logic::Strategy strategy> struct isStratA {
        static const bool value = strategy==Logic::strat_A;
    };

    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<Logic::isStratA<strategy>::value,int>
    computeThings();

    template <class A, class B, Logic::Strategy strategy>
    typename std::enable_if_t<!Logic::isStratA<strategy>::value,int>
    computeThings();
};

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<Logic::isStratA<strategy>::value,int>
Logic::computeThings() {
    return 0;
}

template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<!Logic::isStratA<strategy>::value,int>
Logic::computeThings() {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

с Вариантом 3, обаClang и GCC счастливы.Тем не менее, я не, так как я должен создать много фиктивных оберток по неизвестной причине (здесь, у меня есть только один, но технически, я должен иметь и isStratA<> и isStratB<>).

Вопросы:

  • нарушаю ли я какой-либо стандарт C ++ (или здравый смысл) в своем варианте 2?
  • у меня есть простой способ заставить работать решение типа Variant 2, не переходя кфиктивные оболочки, как в варианте 3?

(если это имеет значение, GCC 7.4.0 и Apple LLVM версии 10.0.0: clang-1000.11.45.5)

Ответы [ 2 ]

0 голосов
/ 03 марта 2019

Как сказал @bogdan в комментариях, это, скорее всего, ошибка компилятора.На самом деле я заметил, что это работает, если вы используете конечные типы возвращаемых данных в определениях вне строки ваших шаблонов функций:

template <class A, class B, Logic::Strategy strategy>
auto Logic::computeThings() ->
std::enable_if_t<strategy==Logic::strat_A,int> {
    return 0;
}

template <class A, class B, Logic::Strategy strategy>
auto Logic::computeThings() ->
std::enable_if_t<strategy==Logic::strat_B,int> {
    return 1;
}

Я предпочитаю ставить enable_if в типе нетипового шаблонапараметр с аргументом по умолчанию:

template <class A, class B, Logic::Strategy strategy,
          std::enable_if_t<strategy==Logic::strat_A,int> = 0>
int Logic::computeThings() {
    return 0;
}

template <class A, class B, Logic::Strategy strategy,
          std::enable_if_t<strategy==Logic::strat_B,int> = 0>
int Logic::computeThings() {
    return 1;
}

Но SFINAE - слишком сложная функция для чего-то такого простого.Есть гораздо более простые способы сделать то, что вы пытаетесь сделать.Возьмем этот пример с использованием тега dispatch:

#include <iostream>
#include <type_traits>

class Logic {
public:
    enum Strategy { strat_A, strat_B };

    template <class A, class B>
    int computeThings(std::integral_constant<Strategy, strat_A>);

    template <class A, class B>
    int computeThings(std::integral_constant<Strategy, strat_B>);
};

template <class A, class B>
int Logic::computeThings(std::integral_constant<Strategy, strat_A>) {
    return 0;
}

template <class A, class B>
int Logic::computeThings(std::integral_constant<Strategy, strat_B>) {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int>(
            std::integral_constant<Logic::Strategy, Logic::strat_A>{}
        )<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int>(
            std::integral_constant<Logic::Strategy, Logic::strat_B>{}
        )<<std::endl; //outputs 1
    return 0;
}

Это можно еще больше упростить, избавившись от перечисления и определив вместо этого некоторые типы тегов:

class Logic {
public:
    class strat_A {};
    class strat_B {};

    template <class A, class B>
    int computeThings(strat_A);

    template <class A, class B>
    int computeThings(strat_B);
};

template <class A, class B>
int Logic::computeThings(strat_A) { return 0; }

template <class A, class B>
int Logic::computeThings(strat_B) { return 1; }

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int>(Logic::strat_A{})<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int>(Logic::strat_B{})<<std::endl; //outputs 1
    return 0;
}

Более идиоматический и структурированный подходк шаблону стратегии можно отнести поведение различных стратегий из функции computeThings в сами классы стратегий:

class Logic {
public:
    struct strat_A {
        template <class A, class B>
        static int computeThings(Logic* self);
    };
    struct strat_B {
        template <class A, class B>
        static int computeThings(Logic* self);
    };

    template <class A, class B, class Strategy>
    int computeThings() {
        return Strategy::template computeThings<A, B>(this);
    }
};

template <class A, class B>
int Logic::strat_A::computeThings(Logic* self) {
    return 0;
}

template <class A, class B>
int Logic::strat_B::computeThings(Logic* self) {
    return 1;
}

int main() {
    Logic mylogic;
    std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
    std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
    return 0;
}

Указатель Logic* self в этом примере не требуется,но было бы, если стратегии должны получить доступ к экземпляру Logic.

0 голосов
/ 03 марта 2019

TLDR : примеры 2 и 3 могут быть плохо сформированными.

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


enable_if используется для удаления перегрузки из набора перегрузкииспользуя правила [over.match.funcs] / 7 и [temp.deduct] / 8 .

[over.match.funcs] /7 :

В каждом случае, когда кандидат является шаблоном функции, специализации шаблона функции кандидата генерируются с использованием вывода аргумента шаблона ( [temp.over] , [temp.deduct] ).Если шаблон конструктора или шаблон функции преобразования имеет явный спецификатор, константное выражение которого зависит от значения ( [temp.dep] ), сначала выполняется вывод аргумента шаблона, а затем, если контекст требует кандидатаэто не является явным, и сгенерированная специализация является явной ( [dcl.fct.spec] ), она будет удалена из набора кандидатов....

Слово constructor не означает конструктор класса, это внутренний механизм компилятора, который генерирует перегрузку. [over.match.funcs] / 1

[temp.deduct] / 8 :

При заменеприводит к неверному типу или выражению, вывод типа завершается неудачей.Недопустимый тип или выражение - это то, что было бы неправильно сформировано, с необходимостью диагностики, если оно написано с использованием замещенных аргументов.[ Примечание: Если диагностика не требуется, программа все еще не работает.Проверка доступа выполняется как часть процесса замены.- конец примечания ] Может только недопустимые типы и выражения в непосредственном контексте типа функции, типов параметров шаблона и ее явный-спецификатор может привести к ошибке вычета. [ Примечание: Замена на типы и выражения может привести к эффектам , таким как создание экземпляров специализаций шаблонов классов и / или шаблонов функций,генерация неявно определенных функций и т. д. Такие эффекты не находятся в «непосредственном контексте» и могут привести к некорректной работе программы .- конечная нота ] ...

Существует проблема, поскольку стандарт не определяет, что такое immediate context. DR1844 Что такое непосредственный контекст?

Но шаблон-параметр strategy не выводится в [temp.deduct.call] / 5.

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

И я думаю, что его замена происходит вне непосредственного контекста, что делает ваши второй и третий примеры плохо сформированными.

...