我试图弄清楚是否可以将概念用作类的一种接口,而不需要虚拟表的开销。我整理了一个可以工作的示例,但是我必须将类实例存储在由它们的通用继承而不是它们的通用概念定义的数组中。我没有看到有关概念数组的文章中讨论过任何内容,但是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文件以包含C ++ 17标准定义的apply(),但可在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;
};
答案 0 :(得分:4)
这无法完成。
我的意思是您可以实现自己的类型擦除,以替换virtusl函数表。在您的特定情况下,它可能比vtable更具性能,因为您可以根据自己的确切问题来使用它。
要从编译器获取帮助,而不必编写样板代码/胶水代码,则需要附带概念的反射和版本化支持。
如果这样做,它将看起来像:
ShapePtr shapes[2] = {&square, &rect};
或
ShapeValue shapes[2] = {square, rect};
现在,这并不能满足您希望表现明智的一切;类型擦除仍将在函数指针中跳转。并具有每个对象或查看存储开销。您可以将更多的存储空间换成更少的间接空间。
这里的手动类型擦除基本上是在C中实现一个对象模型,然后将其包装为C ++。默认的C ++对象模型只是一种可能的方法,C程序实现了许多替代方法。
您还可以退后一步,将数组替换为元组。元组可以存储非统一类型,并且通过大量工作可以遍历它们:
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
。
答案 1 :(得分:0)
我知道您正在尝试做什么,但这对您的用例没有意义。概念是在编译时强制执行接口的方法,通常用于模板功能。您想要的是一个抽象接口-一个带有一些纯虚拟成员函数的基类。
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
}
在一个旁注中,也许这只是一个人为的例子,但是从对象的角度来说,正方形肯定不是矩形。一个简单的示例是,有人可以在矩形基类上包含一个名为stretch
的方法,而您必须在正方形中实现它。当然,只要您拉伸任意尺寸的正方形,它便不再是正方形。小心点。
答案 2 :(得分:0)
Y牛的答案是正确的,但我觉得它太复杂了。 从某种意义上来说,您的要求是错误的,您正在尝试获得“免费”的东西,而您无法免费获得这些东西:
我试图弄清楚是否可以将概念用作类的一种接口,而不需要虚拟表的开销。
答案是否定的。这并不是因为虚拟表的开销是一些不必要的成本。
如果要使用一组Shapes来使用它们,则需要存储有关特定实例的信息。
虚拟机为您做到这一点(最简单的思考方法是,每个实例都有一个隐藏的枚举成员,该枚举成员在运行时告诉编译器要调用哪些成员函数),如果您希望可以手动进行操作,但必须以某种方式进行操作(例如,您可以使用std::variant<Square,Rectangle>
)。
如果不这样做,则指向Shapes的指针数组与指向void的指针数组一样好。您不知道指针指向什么。
注意:如果由于虚拟开销而确实在性能上苦苦挣扎,请考虑使用Boost polly_collection