在 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
可以获取方法的名称,并从 &C::M
的类型中推断出参数,然后转发到 lambda。是的,这涵盖了危险的部分。 &
&&
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<SimpleTask> []std::cout << "I ran\n";
作为SimpleTask
的基于lambda 的实现。
同样,LambdaDoTask<ComplexTask> [](auto a, auto b) return a+b;
。
这不是很像 Java。 Java 是一种比 C++ 更以 OO 为中心的语言;在 C++ 中,OO 设计是一个选项。
C++ lambdas 创建覆盖operator()
的可调用对象。如果您有“可以使用签名运行”的内容,那么在 C++ 中执行此操作的惯用方法是使用 std::function<void()>
或类似名称。
std::function
使用值语义;如果需要,您可以在内部存储一个指针。
所以在 C++ 中你会想要:
using SimpleTask = std::function<void()>;
您的其余代码现在很简单:
Thread myThread(L"MyTestingThread", [] /* code */ );
myThread.start();
因为如果签名兼容,则 lambda 可以直接转换为 std::function<void()>
。
其中一部分是迁移到价值语义。
但除此之外,你会想要
【讨论】:
感谢您的所有建议!以上是关于在 C++11 或以上,有没有办法通过 lambda 实现单方法纯虚拟 C++ 接口?的主要内容,如果未能解决你的问题,请参考以下文章
有没有办法让我的图像通过 MySQL 或 phpMyAdmin 显示在我的 PHP 页面上?