C++ 从CRTP自动定义访客(CRTP使用boost foreach和boost变体)
我需要实现对实现相同接口的对象向量的有效访问。到目前为止,我一直在使用虚拟函数的继承:接口被定义为一个抽象类,包含纯虚拟函数,每个对象类都实现虚拟函数。对象的向量只是抽象类上指针的向量(有关动态访问的示例,请参见消息末尾) 我需要更快地访问对象集合。因为我在编译时知道所有可能的对象类,所以我使用boost::variant实现对象集合(即boost::variant的向量)。我需要一个访客计划的额外定义来检查集合。为了明确所有对象都实现相同的接口,我使用了一个CRTP来获得静态继承:接口是一个CRTP抽象,每个对象类都派生自模板化的CRTP抽象类 下面是CRTP实现的一个示例。该接口只定义了两个函数C++ 从CRTP自动定义访客(CRTP使用boost foreach和boost变体),c++,variant,crtp,visitor,boost-foreach,C++,Variant,Crtp,Visitor,Boost Foreach,我需要实现对实现相同接口的对象向量的有效访问。到目前为止,我一直在使用虚拟函数的继承:接口被定义为一个抽象类,包含纯虚拟函数,每个对象类都实现虚拟函数。对象的向量只是抽象类上指针的向量(有关动态访问的示例,请参见消息末尾) 我需要更快地访问对象集合。因为我在编译时知道所有可能的对象类,所以我使用boost::variant实现对象集合(即boost::variant的向量)。我需要一个访客计划的额外定义来检查集合。为了明确所有对象都实现相同的接口,我使用了一个CRTP来获得静态继承:接口是一个C
f()
和g(double)
。有两个派生类C1
和C2
实现接口(行为相同)
谢谢你的帮助。对于感兴趣的读者,下面是使用虚拟继承的相同代码
namespace testDynamicSimple
{
struct CBase
{
virtual void f() = 0;
virtual int g(const double & x) = 0;
};
struct C1 : public CBase
{
void f() {}
int g(const double & x) { return 1; }
};
struct C2 : public CBase
{
void f() {}
int g(const double & x) { return 2; }
};
bool test(int nbSample)
{
typedef boost::shared_ptr<CBase> CV;
std::vector<CV> vec;
for( int i=0;i<nbSample;++i )
{
switch( std::rand() % 5 )
{
case 1: vec.push_back( CV(new C1()) ); break;
case 2: vec.push_back( CV(new C2()) ); break;
}
}
double argdouble = 0.0;
BOOST_FOREACH( CV & c, vec)
{
c->f();
c->g(argdouble);
}
}
}
namespace testDynamicSimple
{
结构数据库
{
虚空f()=0;
虚整数g(常数双和x)=0;
};
结构C1:公共CBase
{
void f(){}
int g(const double&x){return 1;}
};
结构C2:公共CBase
{
void f(){}
int g(const double&x){return 2;}
};
布尔测试(样本)
{
typedef boost::共享_ptr CV;
std::vec;
for(int i=0;if();
c->g(argdouble);
}
}
}
免责声明:这不是答案,只是解决给定问题的不同方法的基准。
我们的想法是将boost::variant
与其他技术(原始指针上的虚拟函数、共享指针上的虚拟函数以及一些其他技术)进行比较。下面是我使用的基准代码:
#include <vector>
#include <boost/foreach.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/variant.hpp>
#include <memory>
#include <type_traits>
namespace novirtual {
struct C {
void f() {}
int g(const double &x) { return 1; }
};
void test(int nbSample) {
std::vector<C> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i)
vec.emplace_back(C());
double argdouble = 0.0;
BOOST_FOREACH(C &c, vec) {
c.f();
c.g(argdouble);
}
}
}
namespace crtp {
// Definition of the interface (abstract class).
template <typename C> struct CBase {
void f() { static_cast<C &>(*this).f(); }
int g(const double &x) { return static_cast<C &>(*this).g(x); }
};
// Definition of the first implementation.
struct C1 : public CBase<C1> {
void f();
int g(const double &x);
};
void C1::f() { return; }
int C1::g(const double &x) { return sizeof(x); }
// Definition of the second implementation.
struct C2 : public CBase<C2> {
void f();
int g(const double &x);
};
void C2::f() { return; }
int C2::g(const double &x) { return sizeof(x); }
// Definition of the visitor for the first function of the interface.
class f_visitor : public boost::static_visitor<int> {
public:
template <typename C> int operator()(CBase<C> &c) const {
c.f();
return 0;
}
};
// Definition of the visitor for the second function of the interface.
struct g_visitor : public boost::static_visitor<int> {
const double &x;
g_visitor(const double &x) : x(x) {}
public:
template <typename C> int operator()(CBase<C> &c) const { return c.g(x); }
};
// Example of use: construct a random collection and visit it.
void test(int nbSample) {
typedef boost::variant<C1, C2> CV;
std::vector<CV> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.push_back(C1());
break;
case 1:
vec.push_back(C2());
break;
}
}
double argdouble;
BOOST_FOREACH(CV & c, vec) {
boost::apply_visitor(f_visitor(), c);
g_visitor g(argdouble);
boost::apply_visitor(g, c);
}
}
}
namespace virt_fun {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
};
struct C1 : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
void test(int nbSample) {
std::vector<CBase *> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.push_back(new C1());
break;
case 1:
vec.push_back(new C2());
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(CBase * c, vec) {
c->f();
c->g(argdouble);
}
}
}
namespace shared_ptr {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
};
struct C1 : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
void test(int nbSample) {
typedef boost::shared_ptr<CBase> CV;
std::vector<CV> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.push_back(CV(new C1()));
break;
case 1:
vec.push_back(CV(new C2()));
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(CV & c, vec) {
c->f();
c->g(argdouble);
}
}
}
namespace virt_cont {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
virtual ~CBase() = default;
};
struct C1 final : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 final : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
struct foo {
std::aligned_storage<sizeof(C2)>::type buf;
CBase *ptr;
foo(C1 c) { ptr = new ((void *)&buf) C1(c); }
foo(C2 c) { ptr = new ((void *)&buf) C2(c); }
foo(foo &&x) : buf(x.buf) { ptr = reinterpret_cast<CBase *>(&buf); } // UB
foo &operator=(foo &&x) {
buf = x.buf;
return *this;
} // maybe UB?
~foo() { ptr->~CBase(); }
};
void test(int nbSample) {
std::vector<foo> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.emplace_back(C1());
break;
case 1:
vec.emplace_back(C2());
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(foo & c, vec) {
c.ptr->f();
c.ptr->g(argdouble);
}
}
}
namespace locals {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
virtual ~CBase() = default;
};
struct C1 final : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 final : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
void test(int nbSample) {
C1 c1;
C2 c2;
std::vector<CBase *> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec[i] = &c1;
break;
case 1:
vec[i] = &c2;
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(CBase * c, vec) {
c->f();
c->g(argdouble);
}
}
}
#include <chrono>
#include <string>
#define VER 4
int main(int argc, char *argv[]) {
int n = 100000;
for (int i = 0; i < 4; ++i) {
std::chrono::time_point<std::chrono::system_clock> start, end;
start = std::chrono::system_clock::now();
#if VER == 0
virt_fun::test(n);
#elif VER == 1
shared_ptr::test(n);
#elif VER == 2
crtp::test(n);
#elif VER == 3
virt_cont::test(n);
#elif VER == 4
locals::test(n);
#endif
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
std::time_t end_time = std::chrono::system_clock::to_time_t(end);
std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
n *= 10;
}
return 0;
}
并在Macbook Pro上运行,配备2.6 GHz Intel Core i7处理器和16 GB 1600 MHz DDR3 ram
这些结果考虑了对原始代码的错误修复,以及@dyp提供的附加代码,该代码使用了带有std::aligned_storage
的包装类。
在下表中,no virtual
列完全不对应继承,并作为参考给出
| size | no virtual| raw ptr | shared_ptr | variant | wrapper | locals |
|-----------|-----------|------------|-----------|-----------|-----------|-----------|
| 100000 | 0.000235s | 0.008309s | 0.030801s | 0.003935s | 0.004222s | 0.001925s |
| 1000000 | 0.002053s | 0.061843s | 0.288403s | 0.029874s | 0.033697s | 0.01478s |
| 10000000 | 0.017687s | 0.627659s | 2.91868s | 0.29699s | 0.322109s | 0.141245s |
| 100000000 | 0.177425s | 6.2493s | 28.9586s | 3.00427s | 3.21402s | 1.40478s |
在这个阶段,有一点是肯定的:
boost::shared\u ptr
非常慢,而且在boost::variant
和包装器之间没有明显的赢家。“下面是使用虚拟继承的相同代码。”您的意思是使用虚拟函数。虚拟继承与此不同:我不太明白在第一个代码示例中使用基类模板的原因。@dyp:谢谢您的相关评论。因此,我更正了文本。使用CRTP是为了使接口显式化。实际上,正如您所说,在第一个示例中不需要这样做代码示例,因为对象之间的关系是由访问者处理的。我想利用CRTP显式描述接口的事实来简化访问者方案。更准确地说,查看访问者代码,即使冗长也相当简单。我“觉得”应该有一种更简单的“自动”方式从CRTP定义它们,但我找不到。啊,我明白了。通过应用访问者,可以从运行时删除的类型还原具体类型。也就是说,FOREACH循环中的代码必须“支持”多种不同的类型。要做到这一点,需要重载。在C++1y中,可以使用多态lambda,类似于apply_visitor([&](auto&x){x.f();x.g(argdouble);},c)
。在C++03和C++11中,你运气不好,因为你无法定义本地模板。我认为可以使用虚拟函数而不影响性能,因为在任何情况下都需要一个间接寻址。shared\u ptr
可能是罪魁祸首。感谢你的回答。我尝试过使用或不使用boost::shared\u ptr。没有问题显著差异。在Xeon 2.7GHz和gcc 4.6上的计时如下。对于100个元素的向量,如果使用虚拟函数(对于boost::shared或c指针),则为0.3微秒;如果使用0(不可测量)对于boost::variant with CRTP.Nota:0.3us与我相关,因为整个算法在同一台计算机上花费了大约5us。这不是一个答案。可以说,它应该是一个注释(必须更短)。--虚拟函数通常不会“慢”因为间接性,但是因为错过了内联的机会。在没有优化的情况下编译时,根本不会发生内联(=>函数调用的成本,调用站点的代码优化更少)。我想,boost::variant
甚至执行一些调度步骤,这需要几个函数调用。这些步骤将在-O3
@dyp下消除。我无法将所有这些信息放在注释中……我不是想回答这个问题,我只是想了解同样的问题。nmsd不必检查我的答案a答案是正确的。我甚至无法运行10^9轮测试(使用g++):共享的ptr占用16字节,加上每个新的实例8字节(可能加上开销),即至少24*10^9字节,除非编译器可以优化其中的一些分配。请注意,OP的虚拟函数代码中有两个错误:一个是%5
而不是%2
,一个是bool
返回类型而不是void
。我修复了bool类型,但另一个我没有检查。不过,我很惊讶,一个是我想知道,如果有更多的e,为什么要使用虚函数方法呢
#include <vector>
#include <boost/foreach.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/variant.hpp>
#include <memory>
#include <type_traits>
namespace novirtual {
struct C {
void f() {}
int g(const double &x) { return 1; }
};
void test(int nbSample) {
std::vector<C> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i)
vec.emplace_back(C());
double argdouble = 0.0;
BOOST_FOREACH(C &c, vec) {
c.f();
c.g(argdouble);
}
}
}
namespace crtp {
// Definition of the interface (abstract class).
template <typename C> struct CBase {
void f() { static_cast<C &>(*this).f(); }
int g(const double &x) { return static_cast<C &>(*this).g(x); }
};
// Definition of the first implementation.
struct C1 : public CBase<C1> {
void f();
int g(const double &x);
};
void C1::f() { return; }
int C1::g(const double &x) { return sizeof(x); }
// Definition of the second implementation.
struct C2 : public CBase<C2> {
void f();
int g(const double &x);
};
void C2::f() { return; }
int C2::g(const double &x) { return sizeof(x); }
// Definition of the visitor for the first function of the interface.
class f_visitor : public boost::static_visitor<int> {
public:
template <typename C> int operator()(CBase<C> &c) const {
c.f();
return 0;
}
};
// Definition of the visitor for the second function of the interface.
struct g_visitor : public boost::static_visitor<int> {
const double &x;
g_visitor(const double &x) : x(x) {}
public:
template <typename C> int operator()(CBase<C> &c) const { return c.g(x); }
};
// Example of use: construct a random collection and visit it.
void test(int nbSample) {
typedef boost::variant<C1, C2> CV;
std::vector<CV> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.push_back(C1());
break;
case 1:
vec.push_back(C2());
break;
}
}
double argdouble;
BOOST_FOREACH(CV & c, vec) {
boost::apply_visitor(f_visitor(), c);
g_visitor g(argdouble);
boost::apply_visitor(g, c);
}
}
}
namespace virt_fun {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
};
struct C1 : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
void test(int nbSample) {
std::vector<CBase *> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.push_back(new C1());
break;
case 1:
vec.push_back(new C2());
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(CBase * c, vec) {
c->f();
c->g(argdouble);
}
}
}
namespace shared_ptr {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
};
struct C1 : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
void test(int nbSample) {
typedef boost::shared_ptr<CBase> CV;
std::vector<CV> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.push_back(CV(new C1()));
break;
case 1:
vec.push_back(CV(new C2()));
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(CV & c, vec) {
c->f();
c->g(argdouble);
}
}
}
namespace virt_cont {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
virtual ~CBase() = default;
};
struct C1 final : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 final : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
struct foo {
std::aligned_storage<sizeof(C2)>::type buf;
CBase *ptr;
foo(C1 c) { ptr = new ((void *)&buf) C1(c); }
foo(C2 c) { ptr = new ((void *)&buf) C2(c); }
foo(foo &&x) : buf(x.buf) { ptr = reinterpret_cast<CBase *>(&buf); } // UB
foo &operator=(foo &&x) {
buf = x.buf;
return *this;
} // maybe UB?
~foo() { ptr->~CBase(); }
};
void test(int nbSample) {
std::vector<foo> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec.emplace_back(C1());
break;
case 1:
vec.emplace_back(C2());
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(foo & c, vec) {
c.ptr->f();
c.ptr->g(argdouble);
}
}
}
namespace locals {
struct CBase {
virtual void f() = 0;
virtual int g(const double &x) = 0;
virtual ~CBase() = default;
};
struct C1 final : public CBase {
void f() {}
int g(const double &x) { return 1; }
};
struct C2 final : public CBase {
void f() {}
int g(const double &x) { return 2; }
};
void test(int nbSample) {
C1 c1;
C2 c2;
std::vector<CBase *> vec;
vec.reserve(nbSample);
for (int i = 0; i < nbSample; ++i) {
switch (std::rand() % 2) {
case 0:
vec[i] = &c1;
break;
case 1:
vec[i] = &c2;
break;
}
}
double argdouble = 0.0;
BOOST_FOREACH(CBase * c, vec) {
c->f();
c->g(argdouble);
}
}
}
#include <chrono>
#include <string>
#define VER 4
int main(int argc, char *argv[]) {
int n = 100000;
for (int i = 0; i < 4; ++i) {
std::chrono::time_point<std::chrono::system_clock> start, end;
start = std::chrono::system_clock::now();
#if VER == 0
virt_fun::test(n);
#elif VER == 1
shared_ptr::test(n);
#elif VER == 2
crtp::test(n);
#elif VER == 3
virt_cont::test(n);
#elif VER == 4
locals::test(n);
#endif
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
std::time_t end_time = std::chrono::system_clock::to_time_t(end);
std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
n *= 10;
}
return 0;
}
$ clang++ --version
Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.3.0
Thread model: posix
| size | no virtual| raw ptr | shared_ptr | variant | wrapper | locals |
|-----------|-----------|------------|-----------|-----------|-----------|-----------|
| 100000 | 0.000235s | 0.008309s | 0.030801s | 0.003935s | 0.004222s | 0.001925s |
| 1000000 | 0.002053s | 0.061843s | 0.288403s | 0.029874s | 0.033697s | 0.01478s |
| 10000000 | 0.017687s | 0.627659s | 2.91868s | 0.29699s | 0.322109s | 0.141245s |
| 100000000 | 0.177425s | 6.2493s | 28.9586s | 3.00427s | 3.21402s | 1.40478s |