В C ++ 11 или выше, есть ли способ реализовать чисто виртуальный интерфейс C ++ с одним методом с помощью лямбды? - PullRequest
7 голосов
/ 22 мая 2019

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

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

class SimpleTask
{
public:
    void run();
};

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

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 для реализации интерфейса с одним методом , чтобы писать код более лаконично, чем при использовании анонимного класса. Я немного исследовал C ++ 11 и не нашел ничего похожего на это.

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

Ответы [ 4 ]

11 голосов
/ 22 мая 2019

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

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);
4 голосов
/ 22 мая 2019

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

std::thread t(
  [](){
    std::cout << "thread\n"; // Here is the code run by the thread...
  }
);
std::cout << "main\n";
t.join();
2 голосов
/ 22 мая 2019

Старый Виртуальный стиль интерфейса:

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);

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

// 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();
    ...
}
1 голос
/ 22 мая 2019

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

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 квалификационных отборочных, раз 2 квалификационных отборочных, временные 2 отборочных квалификационных, времена не исключая истину / ложь для 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 гораздо более OO-ориентированный язык, чем C ++;в C ++ OO-дизайн - это опция .

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

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

Так что в C ++ вы захотите:

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...