我试图弄清楚我是否可以将概念用作类的接口,而不需要虚拟表的开销。我把一个实例组合在一起,但是我必须将我的类实例存储在由它们的公共继承而不是它们的共同概念定义的数组中。我没有看到关于概念数组的帖子中讨论的任何内容,但g ++ 6.3.0似乎不允许它。错误是:
$ g++ -fconcepts -std=c++1z custom_concept.cpp
custom_concept.cpp: In function ‘int main()’:
custom_concept.cpp:37:20: error: ‘shapes’ declared as array of ‘IShape*’
IShape* shapes[2] = {&square, &rect}; // doesn't work
^
custom_concept.cpp:39:25: error: ‘shapes’ was not declared in this scope
for (IShape* shape : shapes )
^~~~~~
如果我将IShape*
数组更改为Rectangle*
数组(如导致第一个错误的数组下方的注释行),程序将按预期编译并运行。
为什么不允许使用概念指针数组?这可能会在未来的c ++版本中被允许吗?
(我的例子包括虚函数和继承,即使我的目标是消除它们。我只是为了方便使Rectangle*
版本工作而包含它们。如果我能让IShape*
版本工作,我打算删除虚函数和继承。)
这是代码:
#include <iostream>
template <typename T>
concept bool IShape = requires (T x, T z, int y)
{
{ T() } ;
{ T(x) } ;
{ x = z } -> T& ;
{ x.countSides() } -> int ;
{ x.sideLength(y) } -> int ;
};
struct Rectangle
{
Rectangle() {};
Rectangle(const Rectangle& other) {};
Rectangle& operator=(Rectangle& other) {return *this; };
virtual std::string getName() { return "Rectangle"; }
int countSides() {return 4;}
virtual int sideLength(int side) { return (side % 2 == 0) ? 10 : 5; }
};
struct Square : public Rectangle
{
Square() {};
Square(const Square& other) {};
Square& operator=(Square& other) {return *this; };
std::string getName() override { return "Square"; }
int sideLength(int side) override { return 10; }
};
int main()
{
Square square;
Rectangle rect;
IShape* shapes[2] = {&square, &rect}; // doesn't work
// Rectangle* shapes[2] = {&square, &rect}; // works
for (IShape* shape : shapes )
{
for (int side = 0 ; side < shape->countSides() ; ++side )
{
std::cout << shape->getName() << " side=" << shape->sideLength(side) << "\n";
}
}
return 0;
};
感谢@Yakk关于使用元组的想法。 G ++ 6.3.0没有完全实现#include文件以包含apply(),正如C ++ 17标准定义的那样,但它在std :: experimental中可用。 (我认为它可能会在g ++的更高版本中添加。)这是我最终得到的结果:
#include <iostream>
#include <tuple>
#include <experimental/tuple>
template <typename T>
concept bool IShape = requires (T x, T z, int y)
{
{ T() } ;
{ x = z } -> T& ;
{ T(x) } ;
{ x.countSides() } -> int ;
{ x.sideLength(y) } -> int ;
};
struct Rectangle
{
Rectangle() {};
Rectangle(const Rectangle& other) {};
Rectangle& operator=(Rectangle& other) {return *this; };
std::string getName() { return "Rectangle"; }
int countSides() {return 4;}
int sideLength(int side) { return (side % 2 == 0) ? 10 : 5; }
};
struct Square
{
Square() {};
Square(const Square& other) {};
Square& operator=(Square& other) {return *this; };
std::string getName() { return "Square"; }
int countSides() {return 4;}
int sideLength(int side) { return 10; }
};
void print(IShape& shape)
{
for (int side = 0 ; side < shape.countSides() ; ++side )
{
std::cout << shape.getName() << " side=" << shape.sideLength(side) << "\n";
}
};
int main()
{
Square square;
Rectangle rect;
auto shapes = std::make_tuple(square, rect);
std::experimental::apply([](auto&... shape) { ((print(shape)), ...); }, shapes) ;
return 0;
};
这是不可能做到的。
我的意思是你可以实现自己的类型擦除来取代virtusl函数表。在特定情况下,它可能比vtable更具性能,因为你可以针对你的确切问题进行讨论。
要从编译器获得帮助以便您不必编写样板/粘合代码,您需要在侧面概念上提供反射和实现支持。
如果你这样做,它看起来像:
ShapePtr shapes[2] = {&square, &rect};
要么
ShapeValue shapes[2] = {square, rect};
现在这不会做你希望表现明智的一切;类型擦除仍然会跳过函数指针。并拥有每个对象或视图存储开销。但是,您可以交换更多存储空间以减少间接。
这里的手动类型擦除基本上是在C中实现一个对象模型,然后将它包装在C ++中看起来很漂亮。默认的C ++对象模型只是一种可能的方法,C程序实现了许多替代方案。
你也可以后退一步,用一个元组替换数组。元组可以存储非统一类型,通过bkt工作,您可以迭代它们:
auto shapes = make_IShapePtr_tuple(&square, &rect);
foreach_elem( shapes,[&](IShape* shape )
{
for (int side = 0 ; side < shape->countSides() ; ++side )
{
std::cout << shape->getName() << " side=" << shape->sideLength(side) << "\n";
}
});
lambda获取非类型擦除类型。
这些都不需要概念:
auto shapes = std::make_tuple(&square, &rect);
foreach_elem( shapes,[&](auto* shape )
{
for (int side = 0 ; side < shape->countSides() ; ++side )
{
std::cout << shape->getName() << " side=" << shape->sideLength(side) << "\n";
}
});
以上可以写在c++14。
一个c++17 foreach_elem
看起来像:
template<class T, class F>
void foreach_elem( T&& t, F&& f ) {
std::apply( [&](auto&&...args){
( (void)f(decltype(args)(args)), ... );
}, std::forward<T>(t) );
}
在c++14中,lambda中的行代替:
using discard=int[];
(void)discard{ 0,((void)f(decltype(args)(args)),0)... };
这有点迟钝,需要实施std::apply
。
我看到你想要做什么,但它对你的用例没有意义。概念是在编译时强制执行接口的方法,通常用于模板函数。你想要的是一个抽象接口 - 一个带有一些纯虚拟成员函数的基类。
template <ShapeConcept S, ShapeConcept U>
bool collide(S s, U u)
{
// concrete types of S and U are known here
// can use other methods too, and enforce other concepts on the types
}
抽象接口在运行时强制执行接口 - 您不直接知道具体类型是什么,但您可以使用提供的方法。
bool collide(ShapeInterface& s, ShapeInterface& u)
{
// concrete types of S and U are unknown
// only methods of interfaces are available
}
在旁注中,也许这只是一个人为的例子,但Square在面向对象的意义上肯定不是一个矩形。一个简单的例子是,有人可以在矩形基类上包含一个名为stretch
的方法,你必须在你的方块中实现它。当然,只要您在任何尺寸上拉伸正方形,它就不再是正方形。小心。
Yakk的回答是正确的,但我觉得它太复杂了。从某种意义上说,您的要求是错误的,因为您正试图获得“免费”的东西,而这些东西是您无法免费获得的:
我试图弄清楚我是否可以将概念用作类的接口,而不需要虚拟表的开销。
答案是否定的。并不是因为虚拟表的开销是一些不必要的成本。如果要使用Shapes数组来使用它们,则需要存储有关特定实例的信息。虚拟机器为你做这个(最简单的方法是考虑这个是每个实例的隐藏枚举成员,告诉编译器在运行时调用哪些成员函数),如果你想要你可以手动完成,但你必须以某种方式做到这一点(例如,您可以使用std::variant<Square,Rectangle>
)。
如果你不这样做,那么指向Shapes的指针数组就像指向void的指针数组一样好。你不知道你的指针指向什么。
注意:如果你真的因为虚拟开销而在性能方面遇到困难,可以考虑使用Boost polly_collection