在 C++11 或以上,有没有办法通过 lambda 实现单方法纯虚拟 C++ 接口?

Posted

技术标签:

【中文标题】在 C++11 或以上,有没有办法通过 lambda 实现单方法纯虚拟 C++ 接口?【英文标题】:In C++11 or above, Is there a way to implement a single-method pure virtual C++ interface by lambda? 【发布时间】:2019-05-22 14:54:25 【问题描述】:

我有一个用 C++ 98 编写的大型 C++ 库,它大量使用 C++ 接口(准确地说,只有纯虚函数的 C++ 类)进行事件处理。现在看到我的代码是由 C++11/14 编译器编译的,我在想是否可以通过使用 C++11 lambda 替换接口实现来减少样板代码。

在我的库中,有一些 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 中,we can use Java lambda to implement a single-method interface 编写代码比使用匿名类更简洁。我在 C++11 中做了一些研究,发现没有类似的东西。

由于我的库的事件处理代码是面向对象模式设计的,而不是函数式编码风格,有没有办法使用 lambda 来帮助减少那些单方法接口实现代码?

【问题讨论】:

Java标签真的没有理由 不需要new。通过移动传递它或至少创建一个std::unique_ptr SimpleTask 不是接口。没有虚方法,更别说纯虚方法了。 如果您要对现有的 C++ 进行现代化改造,那么您应该使用现代技术。自 1998 年接口和虚函数成为一件大事以来,C++ 发生了变化。更多(松散地使用该术语)常用技术是使用模板和鸭子类型。如果传递的对象具有适当的接口,则可以使用它。对于 Lambda,接口是函子,例如 operator()(<Argument List>) I did a bit research in C++11 and found there nothing similar to this 除了 C++ Lambda? [<CaptureList>](<ArgList>)<Code> C++ 中最有趣的声明是 []()(); 【参考方案1】:

您可以创建一个包装器,例如:

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 同意,但我怀疑所有接口都匹配void run()。我在答案末尾添加了一些细节。 哎呀,对。最后一点:t 不应该在构造函数和makeSimpleTask 中移动吗? 应该 ;) 而且还应该转发到makeXXX WRAPPER_FOR 可以获取方法的名称,并从 &amp;C::M 的类型中推断出参数,然后转发到 lambda。是的,这涵盖了危险的部分。 &amp; &amp;&amp; const volatile noexcept 过载会让人很烦。【参考方案2】:

这不是你要找的吗?

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 位在您的版本中的位置以使其更清晰 并非如此。 Java 让您在调用方法时内联编写匿名类定义。所以在这种情况下,Java 代码看起来像像这样:registerTask(new SimpleTask void run() \\run; );std::thread 与问题完全无关。 @Yksisarvinen OP 明确提到需要 lambda,而不是匿名类。 OP 似乎有他自己的Thread 类,它在其构造函数中接受SimpleTask 指针。不知道这个答案是如何相关的 @Jean-BaptisteYunès 他们提到了 Java lambda,询问是否可以在 C++ 中完成类似的事情【参考方案3】:

旧虚拟界面风格:

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

【讨论】:

感谢您的所有建议,我需要更新我的现代 C++ 知识才能完全理解您的解决方案,不胜感激!【参考方案4】:

这是一种使用更好的语法来完成@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 限定,24 个不同版本的 noexcept true/false。

现在想象一个宏:

#define RUN_AS_LAMBDA_TYPE( CLASS ) \
  run_as_lambda_impl< CLASS >

这将run 硬编码为我们覆盖的方法,但不对签名进行硬编码。我们还输入了擦除 lambda,但我现在还可以。

我们可以解决这个问题。

#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&lt;SimpleTask&gt; []std::cout &lt;&lt; "I ran\n"; 作为SimpleTask 的基于lambda 的实现。

同样,LambdaDoTask&lt;ComplexTask&gt; [](auto a, auto b) return a+b;


这不是很像 Java。 Java 是一种比 C++ 更以 OO 为中心的语言;在 C++ 中,OO 设计是一个选项

C++ lambdas 创建覆盖operator() 的可调用对象。如果您有“可以使用签名运行”的内容,那么在 C++ 中执行此操作的惯用方法是使用 std::function&lt;void()&gt; 或类似名称。

std::function 使用值语义;如果需要,您可以在内部存储一个指针。

所以在 C++ 中你会想要:

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

您的其余代码现在很简单:

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

因为如果签名兼容,则 lambda 可以直接转换为 std::function&lt;void()&gt;

其中一部分是迁移到价值语义。

但除此之外,你会想要

【讨论】:

感谢您的所有建议!

以上是关于在 C++11 或以上,有没有办法通过 lambda 实现单方法纯虚拟 C++ 接口?的主要内容,如果未能解决你的问题,请参考以下文章

确保当前线程持有 C++11 互斥锁

有没有办法让我的图像通过 MySQL 或 phpMyAdmin 显示在我的 PHP 页面上?

有没有办法通过flutter查看来自云Firestore的用户的关注者或关注者?

有没有办法在运行时通过程序识别代码文件中的保留字?

有没有办法在批处理文件中完成多线程或并行进程?

有没有办法从 C++ 中区分文件?