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

Я пытаюсь понять, почему этот код не компилируется:

// test.h
struct Base
  {
  virtual ~Base{};
  virtual void execute() {}
  virtual void execute(int) {}
  virtual void execute(double) {}
  }

template<class T>
struct Test : Base
  {
    void execute(typename std::enable_if<std::is_void<T>::value, void>::type)
      {
      // Do A
      }

    void execute(typename std::enable_if<!std::is_void<T>::value, int>::type t)
      {
      // Do B
      }
  };

// main.cpp
Test<void> t; 

Я получаю ошибку компилятора: "нет типа с именем типа".

Та же ошибка, даже если я изменю версию кода A с помощью

std::enable_if<std::is_void<T>::value>

Цель состоит в том, чтобы создать класс, который в зависимости от параметра T создает различные члены-функции. В данном случае 2, но мне было бы интересно и больше.

[Изменить] Я добавил часть наследования, о которой говорил в комментариях.


person svoltron    schedule 15.01.2019    source источник
comment
Возможный дубликат Выбор функции-члена с использованием разных условий enable_if   -  person Daniel Langr    schedule 15.01.2019
comment
вопрос отредактирован @DanielLangr   -  person svoltron    schedule 15.01.2019
comment
Пожалуйста, не редактируйте вопрос таким образом, чтобы сделать существующие ответы неверными или неполными. Для получения дополнительных сведений, подобных этому, откройте второй дополнительный вопрос, который может быть связан с первым. (Но оставьте это как есть, теперь, когда есть ответы на обе версии.)   -  person aschepler    schedule 15.01.2019
comment
Хорошо, имеет смысл. Спасибо за предложение   -  person svoltron    schedule 15.01.2019
comment
Трудно выбрать лучший ответ, всем спасибо и извините за редактирование   -  person svoltron    schedule 16.01.2019


Ответы (4)


Создавая экземпляр Test<void>, вы также создавали экземпляры объявлений всех его функций-членов. Это просто базовая реализация. Какие декларации это дает вам? Что-то вроде этого:

void execute(void);
void execute(<ill-formed> t);

Если вы ожидали, что SFINAE молча удалит неправильно сформированную перегрузку, вам нужно помнить, что S означает «замещение». Подстановка аргументов шаблона в параметры (члена) шаблона функции. Ни один из execute не является шаблоном функции-члена. Обе они являются обычными функциями-членами специализации шаблона.

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

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

template<typename T>
struct TestBase {
  void execute(T t) { }
};

template<>
struct TestBase<void> {
  void execute() { }
};

template<class T>
struct Test : private TestBase<T> {
  using TestBase<T>::execute;
};

Вы можете выбрать то, что лучше всего подходит для ваших нужд.


Чтобы исправить ваше редактирование. Я думаю, что второй подход действительно лучше соответствует вашим потребностям.

template<typename T>
struct TestBase : Base {
  void execute(T t) override { }
};

template<>
struct TestBase<void> : Base {
  void execute() override { }
};

TestBase — это посредник, который выполняет то, что вам нужно.

person StoryTeller - Unslander Monica    schedule 15.01.2019
comment
Спасибо за ваш отзыв. Действительно интересное решение. Единственная проблема заключается в том, что если есть другие члены функции, кроме execute(), я должен написать их снова для специализации. Однако, возможно, этот подход работает для проблемы наследования, которую я имел в виду: в основном заставляя Test::execute переопределять виртуальную функцию TestBase::execute. С другой стороны кажется, что виртуальный механизм не работает - person svoltron; 15.01.2019
comment
@svoltron - я не знаю обо всей вашей проблеме, но я не понимаю, почему вам нужно добавлять к этому виртуальные функции. TestBase также может содержать элементы данных для выполнения задачи, для которой предназначен execute. Если вы сделаете это виртуальным, вы также вернетесь к исходной проблеме. Вам нужно выяснить, какой execute находится в TestBase, чтобы переопределить его! - person StoryTeller - Unslander Monica; 15.01.2019
comment
Как я написал в комментарии к ответу @Angew, я каким-то образом введу базовый класс, в этом есть смысл, поверьте мне или предположим, что есть. При прямом наследовании от такого базового класса и использовании подхода Angew/YSC виртуальный вызов не выполняется. Вместо этого вызов функции базового класса. - person svoltron; 15.01.2019
comment
@svoltron - Эм. Я не уверен, что вполне понимаю ваш подход из комментария. Я думаю, вы должны последовать совету YSC. Если вы зададите другой вопрос (надеюсь, по теме Stack Overflow), вы сможете объяснить больше. - person StoryTeller - Unslander Monica; 15.01.2019
comment
конечно, все равно спасибо. Я отредактировал вопрос, но я задам другой, если это возможно - person svoltron; 15.01.2019
comment
Конечно, это помогает, еще не пробовал, но я тебе доверяю, эх. Действительно интересный. Я надеялся, что это возможно с помощью enable_if, но это не так. Спасибо большое - person svoltron; 15.01.2019

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

Поскольку execute не является шаблонной функцией, не может быть задействована функция SFINAE. Действительно, всякий раз, когда создается экземпляр Test<void>, создаются обе версии execute, что приводит к ошибке, которая не является ошибкой вывода шаблона.

Вам нужен шаблон функции (пусть вызовите параметр шаблона U), чтобы воспользоваться преимуществами SFINAE; и поскольку вам нужно использовать аргумент шаблона того же типа Test (T), вы можете указать аргумент по умолчанию U = T):

Решение:

template<class T>
struct Test
{
    template<class U = T>
    std::enable_if_t<std::is_void_v<U>> execute()
    { std::cout << "is_void\n"; }

    template<class U = T>
    std::enable_if_t<!std::is_void_v<U>> execute()
    { std::cout << "!is_void\n"; }
};

Текущая демонстрация

person YSC    schedule 15.01.2019
comment
Спасибо за ваш отзыв! Это единственный способ заставить его работать? Предположим, что класс Test наследуется от TestBase, а функция execute() является виртуальной, будут ли с этим проблемы? - person svoltron; 15.01.2019
comment
@svoltron Да. Виртуальная функция уже существует, и sfinae срабатывает при создании экземпляра шаблона. Что вы можете сделать, так это предоставить шаблоны функций sfinae, вызывающие виртуальную функцию. Но это выходит за рамки данного вопроса. Можешь спросить еще один. - person YSC; 15.01.2019
comment
Ммм, я постараюсь подумать об этом, и если я не получу его, тогда я задам другой вопрос, спасибо - person svoltron; 15.01.2019
comment
@svoltron Пожалуйста. Примечание: ожидается, что вы примете ответ, нажав на галочку под оценкой ответа;) - person YSC; 15.01.2019
comment
@Oliv Правда, но виртуальные функции были добавлены к вопросу после этого ответа. - person aschepler; 15.01.2019
comment
Вопрос был отредактирован так, что execute теперь являются виртуальными функциями. Это решение больше не работает, потому что член шаблонной функции не может быть переопределяющим. - person Oliv; 15.01.2019
comment
моя ошибка, извините за это - person svoltron; 15.01.2019
comment
Ага, спасибо @Oliv. Это отрицательный ответ на вопрос от меня. На мой взгляд, ОП должен был задать другой вопрос. Такое редактирование делает недействительной работу, проделанную для первой версии. - person YSC; 15.01.2019

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

Если бы вы использовали С++ 17, это было бы просто благодаря if constexpr. В С++ 11 альтернативой является использование отправки тегов:

template<class T>
struct Test : Base
  {
    void execute()
      {
      void do_execute(std::integral_constant<bool,std::is_void<T>::value>{});
      }

    void execute(int t)
      {
      void do_execute(std::integral_constant<bool,!std::is_void<T>::value>{}, t);
      }
  private:
  void do_execute(std::integral_constant<bool,true>){
       /*implementation*/
       }
  void do_execute(std::integral_constant<bool,false>){
       Base::execute();//Call directly the base function execute.
                       //Such call does not involve the devirtualization
                       //process.
       }
  void do_execute(std::integral_constant<bool,true>,int t){
       /*implementation*/
       }
  void do_execute(std::integral_constant<bool,false>,int t){
       Base::execute(t);//Call directly the base function execute.
                        //Such call does not involve the devirtualization
                        //process.
       }
  };

С C++17 if constexpr это может выглядеть более элегантно, чем решение CRTP:

template<class T>
struct Test : Base
  {
    void execute(){
      if constexpr (is_void_v<T>){
         Base::execute();
         }
      else{
        /* implementation */
        }
      }

    void execute(int t){
      if constexpr (!is_void_v<T>){
         Base::execute(t);
         }
      else{
        /* implementation */
        }
      }
  };
person Oliv    schedule 15.01.2019
comment
Я использую С++ 11 и, вероятно, CRTP более элегантен, как вы говорите. С C++17 действительно намного лучше - person svoltron; 15.01.2019

Вы можете инкапсулировать различные перегрузки execute в набор связанных вспомогательных классов, примерно так:

template <class T>
struct TestHelper : Base
{
  void execute(int) override {}
};

template <>
struct TestHelper<void> : Base
{
  void execute() override {}
};


template <class T>
struct Test : TestHelper<T>
{
  // Other Test stuff here
};

Если реализация execute на самом деле зависит от «Другого тестового материала», который должен быть разделен между ними, вы также можете использовать CRTP:

template <class T, class Self>
struct TestHelper : Base
{
  void execute(int) override
  {
    Self& self = static_cast<Self&>(*this);
    // Now access all members through `self.` instead of directly
  }
};

template <class Self>
struct TestHelper<void, self> : Base
{
  void execute() override
  {
    Self& self = static_cast<Self&>(*this);
    // Now access all members through `self.` instead of directly
  }
};

template <class T>
struct Test : TestHelper<T, Test>
{
  // Other Test stuff here
};
person Angew is no longer proud of SO    schedule 15.01.2019
comment
Еще раз спасибо за отзыв. Это похоже на @StoryTeller, но включает также часть инъекции CRTP. Так что это, кажется, единственный способ - person svoltron; 15.01.2019
comment
@svoltron Надеюсь, это поможет. Но, пожалуйста, старайтесь избегать проблем XY в будущих вопросах, они очень раздражают. отвечать. - person Angew is no longer proud of SO; 15.01.2019