В С++ 11 или выше, есть ли способ реализовать чистый виртуальный интерфейс С++ с одним методом с помощью лямбда?

У меня есть крупномасштабная библиотека С++, написанная на С++ 98, которая интенсивно использует интерфейс С++ (точнее, классы С++ только с чистыми виртуальными функциями) для обработки событий. Теперь, видя, что мой код скомпилирован компилятором С++ 11/14, я думаю, могу ли я уменьшить шаблонный код, используя лямбду С++ 11 для замены реализации интерфейса.

В моей библиотеке есть несколько интерфейсов C++, у которых есть только один метод, например, следующий интерфейс, который мы использовали для определения простой задачи:

class SimpleTask
{
public:
    virtual void run() = NULL;
};

Мое намерение состоит в том, чтобы использовать C++ lambda для замены старого кода реализации интерфейса с одним методом следующим образом:

void myFunction()
{
    ...

    class MySimpleTask : SimpleTask //An inline class to implement the iterface
    {
    public:
        void run() 
        {
            //Do somthing for this task
            ...    
            delete this;    //Finally, destroy the instance
        }
    };
    MySimpleTask * myThreadTask = new MySimpleTask();
    Thread myThread(L"MyTestingThread", myThreadTask);
    myThread.start();

    ...
}

В Java 8 мы можем использовать Java lambda для реализации интерфейса с одним методом для написания более лаконичного кода, чем использование анонимного класса. Я провел небольшое исследование в С++ 11 и не нашел ничего подобного.

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

на самом деле нет причин для тега Java

Sharon Ben Asher 22.05.2019 16:56

new не надо. Проходи за ходом или хотя бы ставь std::unique_ptr

Quimby 22.05.2019 16:59
SimpleTask это не интерфейс. Не существует виртуального метода, не говоря уже о чисто виртуальном методе.
Yakk - Adam Nevraumont 22.05.2019 17:25

Если вы собираетесь модернизировать существующий C++, вам следует использовать современные методы. C++ изменился с 1998 года, когда большое значение имели интерфейсы и виртуальные функции. Более распространенный метод (используйте этот термин в широком смысле) заключается в использовании шаблонов и утиной печати. Если переданный объект имеет соответствующий интерфейс, вы можете его использовать. Для Lambda интерфейс похож на функтор, т.е. operator()(<Argument List>).

Martin York 22.05.2019 18:16
I did a bit research in C++11 and found there nothing similar to this За исключением, может быть, C++ Lambda? [<CaptureList>](<ArgList>){<Code>} Самая забавная декларация в C++ — это [](){}();
Martin York 22.05.2019 18:47

Проблема в том, что лямбды предназначены для работы с std::function<void()>, а не SimpleTask.

Mooing Duck 23.05.2019 00:11

Спасибо за все входы/предложения. Код, который у меня был, был первоначально написан еще в 2005 году, и есть множество лучших шаблонов C++, которые я могу принять, например, использовать интеллектуальный указатель C++ вместо «нового», требуется время для модернизации всей библиотеки.

Hongkun Wang 23.05.2019 01:23

Извините, метод «запустить» должен иметь сигнатуру «виртуальный недействительный запуск () = 0;», я ошибся, когда писал вопрос.

Hongkun Wang 23.05.2019 01:25
3 метода стилизации элементов HTML
3 метода стилизации элементов HTML
Когда дело доходит до применения какого-либо стиля к нашему HTML, существует три подхода: встроенный, внутренний и внешний. Предпочтительным обычно...
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
7
8
787
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Разве это не то, что вы ищете?

std::thread t(
  [](){
    std::cout << "thread\n"; // Here is the code run by the thread...
  }
);
std::cout << "main\n";
t.join();

Возможно, покажите, где в вашей версии будет бит //Do somthing for this task, чтобы было немного понятнее.

Steve 22.05.2019 16:58

Не совсем. Java позволяет вам написать определение анонимного класса в строке при вызове метода. Таким образом, в этом случае код Java будет выглядеть, например. вот так: registerTask(new SimpleTask { void run() { \\run; }});. std::thread совершенно не связан с вопросом.

Yksisarvinen 22.05.2019 17:00

@Yksisarvinen OP прямо упомянул о необходимости лямбда, а не анонимного класса.

Jean-Baptiste Yunès 22.05.2019 17:01

У OP, кажется, есть свой собственный класс Thread, который принимает указатель SimpleTask в своем конструкторе. Не уверен, как этот ответ связан

R2RT 22.05.2019 17:02

@Jean-BaptisteYunès Они упомянули лямбда-выражение Java, спросив, можно ли сделать что-то подобное на C++.

Yksisarvinen 22.05.2019 17:04

Спасибо за ваши предложения. Пример потока — лишь один из многих «обработчиков событий» или обратных вызовов в моей библиотеке. Так что использование std::thread не ответило на мой вопрос. Все равно цените. :-)

Hongkun Wang 23.05.2019 01:32
Ответ принят как подходящий

Вы можете создать оболочку, например:

class SimpleTask {
public:
    virtual void run() = 0;
};

// This class wraps a lambda (or any callable) and implement the run()
// method by simply calling the callable.
template <class T>
class LambdaSimpleTask: public SimpleTask {
    T t;

public:
    LambdaSimpleTask(T t) : t(std::move(t)) { }

    virtual void run() {
        t();
    }
};


template <class T>
auto makeSimpleTask(T &&t) {
    // I am returning a dynamically allocated object following your example,
    // but I would rather return a statically allocated one.
    return new LambdaSimpleTask<std::decay_t<T>>{std::forward<T>(t)};
}

А затем создать задачу:

auto task = makeSimpleTask([]() { });
Thread myThread(L"MyTestingThread", task);

Обратите внимание, что вам по-прежнему нужна оболочка и функция makeXXX для каждого из ваших интерфейсов. В C++ 17 и более поздних версиях вы можете избавиться от функции makeXXX, используя вывод аргумента шаблона класса. Избавиться от оболочки невозможно, но вы можете уменьшить шаблонный код, инкапсулировав некоторые вещи в макросы.


Вот пример макроса (не идеальный), который можно использовать для сокращения стандартного кода:

#define WRAPPER_FOR(C, M, ...)                       \
    template <class T>                               \
    class Lambda##C: public C {                      \
        T t;                                         \
    public:                                          \
        Lambda##C(T t) : t(std::move(t)) { }         \
        virtual M { return t(__VA_ARGS__); }         \
    };                                               \
    template <class T> auto make##C(T &&t) {         \
        return Lambda##C<std::decay_t<T>>{std::forward<T>(t)}; }

А потом:

class SimpleTask {
public:
    virtual void run() = 0;
};

class ComplexTask {
public:
    virtual int run(int, double) = 0;
};

WRAPPER_FOR(SimpleTask, void run());
WRAPPER_FOR(ComplexTask, int run(int a, double b), a, b);

Поскольку SimpleTask упоминается как пример интерфейса, ваш ответ будет полным, если он будет передан как аргумент шаблона, а не жестко запрограммирован. Изменить настройку: и должны были быть реализованы только правильные makeXXX функции.

R2RT 22.05.2019 17:09

@R2RT Согласен, но я сомневаюсь, что все интерфейсы совпадают void run(). Я добавил некоторые детали в конце ответа.

Holt 22.05.2019 17:11

Упс, верно. И последнее замечание: разве t не следует перемещать в конструкторе, а makeSimpleTask?

R2RT 22.05.2019 17:16

Должна ;) И тоже должна быть переслана в makeXXX.

Holt 22.05.2019 17:17
WRAPPER_FOR может взять имя метода и вывести аргументы из типа &C::M, а затем передать лямбду. И да, это покрывает опасный момент. Это раздражает с &&&constvolatilenoexcept перегрузками.
Yakk - Adam Nevraumont 22.05.2019 17:35

@Yakk-AdamNevraumont Мне было бы интересно, как вы это сделаете для WRAPPER_FOR... Я понятия не имею, как это сделать.

Holt 22.05.2019 18:10

Спасибо за ваше решение, я думаю, что оно делает код немного более кратким. Я не могу понять весь ваш код (особенно часть макроса), но я попробую!

Hongkun Wang 23.05.2019 01:29

Старый стиль виртуального интерфейса:

struct MyInterface {
    virtual Type action(argList)  = 0;
};

class MyClassThatUsesInterface
{
    MyInterface&   interface;
    public:
        MyClassThatUsesInterface(MyInterface& ref)
            : interface(ref)
        {}
        Type doStuff(argList)
        {
             return interface.action(argList);
        }
};
...
MyInterfaceImplementation injectedInterface;
MyClassThatUsesInterface  worker(injectedInterface);
...
worker.doStuff(someStuff);

Более современный стиль:
Или Стиль печати утки:

// No need for an explicit interface definition.
// Any function that will work can be used
// Let the compiler decide if the used function (functor/lambda) works.

template<typename F>
class MyClassThatUsesLambda
{
    F   interface;
    public:
        MyClassThatUsesLambda(F&& ref)
            : interface(std::move(ref))
        {}
        Type doStuff(argList)
        {
             return interface(argList);
             // Will compile if the type F supports function like operations.
             // This means a:
             //   * function pointer.
             //   * std::function
             //   * A type the overloads operator()
             //   * Lambda
        }
};
template<typename F>
MyClassThatUsesLambda<F> make_MyClassThatUsesLambda(F&& f) {return MyClassThatUsesLambda<F>(std::move(f));}
...
auto  worker = make_MyClassThatUsesLambda([](argList){/* Some Stuff*/});
...
worker.doStuff(someStuff);

Глядя на ваш пример (который, кстати, явно не С++)

// Added C++ required virtuals etc:
// Some basic memory management (not checked).
class SimpleTask
{
    public:
        virtual void run() = 0;
};
// Guessed at this object.
class Thread
{
    std::string                    name;
    std::unique_ptr<SimpleTask>    task
    public:
        Thread(std::string const& name, std::unique_ptr<SimpleTask>&& task)
            : name(name)
            , task(std:move(task))
        {}
        void start() {
            task.run();
        }
};
void myFunction()
{
    class MySimpleTask: public SimpleTask
    {
        public:
            virtual void run() override
            {
                //Do something for this task
                ...
                // Destroying this is an exceptionally bad idea.
                // Let the owner destroy it.
                // I made the task hold it as an std::unique_ptr
                // To solve this.    
                // delete this;    //Finally, destroy the instance
            }
    };
    ...
    Thread myThread("MyTestingThread", std::make_unique<MySimpleTask>());
    myThread.start();
    ...
}

Теперь давайте перепишем, используя утиную печать:

template<typename F>
class Thread
{
    std::string                    name;
    F                              task
    public:
        Thread(std::string const& name, F&& task)
            : name(name)
            , task(std:move(task))
        {}
        void start() {
            task();
        }
};
template<typename F>
Thread<F> make_Thread(std::string const& name, F&& f) {return Thread<F>(name, std::move(f));}
void myFunction()
{ 
    ...
    auto  myThread = make_Thread("MyTestingThread", [](argList){/* Do something for this task */});
    myThread.start();
    ...
}

Спасибо за все ваши предложения, мне нужно обновить свои современные знания С++, прежде чем я смогу полностью понять ваше решение, ценю его!

Hongkun Wang 23.05.2019 01:39

Это способ сделать отличный ответ @Holt с немного лучшим синтаксисом. Он не полный, потому что нужно сделать шаблон.

template<class C, class M = decltype(&C::run)>
struct run_as_lambda_impl;

// non-const non-volatile non-ref qualified noexcept(false) case:
template<class C, class R, class...Args>
struct run_as_lambda_impl<C, R(C::*)(Args...)>: C {
  std::function<R(Args...)> f;
  R run(Args...) final override {
    return static_cast<R>(f( std::forward<Args>(args)... ));
  }
};

Вам понадобится 3 уточнения ref, 2 уточнения const, 2 уточнения volatile, умножение noexcept true/false, всего 24 различных версии.

теперь представьте себе макрос:

#define RUN_AS_LAMBDA_TYPE( CLASS ) \
  run_as_lambda_impl< CLASS >

это жестко кодирует run как метод, который мы переопределяем, но не жестко кодирует подпись. Также мы печатаем стирание лямбды, но пока меня это устраивает.

Мы можем обойти это.

#define BASE_LAMBDA_TEMPLATE( NAME, METHOD ) \
  template<class C, class M = decltype(&C::METHOD)> \
  struct NAME

#define LAMBDA_TEMPLATE_IMPL( NAME, METHOD, QUALS ) \
  template<class C, class R, class...Args> \
  struct NAME<C, R(C::*)(Args...) QUALS> : C { \
    std::function<R(Args...)> f; \
    NAME( std::function<R(Args...)> fin ): f(std::move(fin)) {} \
    R METHOD(Args...) QUALS final override { \
      return static_cast<R>( f( std::forward<Args>(args)... ) ); \
    } \
  }

  #define LAMBDA_TEMPLATE( NAME, METHOD ) \
    BASE_LAMBDA_TEMPLATE( NAME, METHOD ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, & ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, && ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, const ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, const & ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, const && ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile & ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile && ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile const ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile const & ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile const && ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, & noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, && noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, const noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, const & noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, const && noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile & noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile && noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile const noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile const & noexcept(true) ); \
    LAMBDA_TEMPLATE_IMPL( NAME, METHOD, volatile const && noexcept(true) )

это 24 различных LAMBDA_TEMPLATE_IMPL вызова для 3*2*2*2*2 типов переопределений методов. Вы можете уменьшить это:

#define LAMBDA_TEMPLATE_IMPL_C( NAME, METHOD, QUALS ) \
  LAMBDA_TEMPLATE_IMPL( NAME, METHOD, QUALS ); \
  LAMBDA_TEMPLATE_IMPL( NAME, METHOD, const QUALS )

#define LAMBDA_TEMPLATE_IMPL_CV( NAME, METHOD, QUALS ) \
  LAMBDA_TEMPLATE_IMPL_C( NAME, METHOD, QUALS ); \
  LAMBDA_TEMPLATE_IMPL_C( NAME, METHOD, volatile QUALS )

#define LAMBDA_TEMPLATE_IMPL_CVR( NAME, METHOD, QUALS ) \
  LAMBDA_TEMPLATE_IMPL_CV( NAME, METHOD, QUALS ); \
  LAMBDA_TEMPLATE_IMPL_CV( NAME, METHOD, & QUALS ); \
  LAMBDA_TEMPLATE_IMPL_CV( NAME, METHOD, && QUALS )

#define LAMBDA_TEMPLATE_IMPL_CVRE( NAME, METHOD ) \
  LAMBDA_TEMPLATE_IMPL_CVR( NAME, METHOD,  ); \
  LAMBDA_TEMPLATE_IMPL_CVR( NAME, METHOD, noexcept(true) )

Потом:

  #define LAMBDA_TEMPLATE( NAME, METHOD ) \
    BASE_LAMBDA_TEMPLATE( NAME, METHOD ); \
    LAMBDA_TEMPLATE_IMPL_CVRE( NAME, METHOD )

что 3+3+3+3+4 = 16 строк вместо 24.


Предположим, у вас есть эти два интерфейса:

class SimpleTask {
public:
  virtual void run() = 0;
};

class ComplexTask {
public:
  virtual int do_stuff(int, double) = 0;
};

тогда вы можете написать

LAMBDA_TEMPLATE( LambdaRun, run );
LAMBDA_TEMPLATE( LambdaDoTask, do_task );

и мы можем использовать LambdaRun<SimpleTask>{ []{std::cout << "I ran\n"; } } как лямбда-реализацию SimpleTask.

Аналогично LambdaDoTask<ComplexTask>{ [](auto a, auto b) { return a+b; } }.


Это не очень похоже на Java. Java гораздо более объектно-ориентированный язык, чем C++; в C++ объектно-ориентированный дизайн — это вариант.

Лямбда-выражения C++ создают вызываемые объекты, которые переопределяют operator(). Если у вас есть что-то, что «может быть запущено с подписью», идиоматический способ сделать это в C++ — использовать std::function<void()> или аналогичный.

std::function использует семантику значений; внутри вы можете сохранить указатель внутри значения, если хотите.

Итак, в С++ вам нужно:

using SimpleTask = std::function<void()>;

и остальная часть вашего кода теперь тривиальна:

Thread myThread(L"MyTestingThread", []{ /* code */} );
myThread.start();

потому что лямбда может быть напрямую преобразована в std::function<void()>, если подписи совместимы.

Часть этого мигрирует к семантике значений.

Но за исключением этого, вы захотите

Спасибо за все ваши предложения!

Hongkun Wang 23.05.2019 01:40

Другие вопросы по теме