概念指针数组
Array of concept pointers
我想弄清楚我是否可以使用概念作为 classes 的一种接口,而不需要虚拟 table 的开销。我整理了一个可以正常工作的示例,但我必须将我的 class 实例存储在一个数组中,该数组由它们的共同继承而不是它们的共同概念定义。我没有在帖子中看到任何关于概念数组的讨论,但 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;
};
我明白你想做什么,但这对你的用例没有意义。概念是在编译时强制执行接口的方法,通常用于模板函数。您在这里想要的是一个抽象接口 - 一个带有一些纯虚拟成员函数的基础 class。
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
}
附带一提,也许这只是一个人为的例子,但正方形肯定不是面向对象意义上的矩形。一个简单的例子是,有人可以在矩形基础 class 上包含一个名为 stretch
的方法,而你必须在你的正方形中实现它。当然,一个正方形在任何维度上一拉伸,它就不再是正方形了。小心。
这做不到。
我的意思是您可以实现自己的类型擦除来替换 virtusl 函数表。在您的特定情况下,它可能比 vtable 性能更高,因为您可以针对您的具体问题对其进行定制。
要从编译器获得帮助,这样您就不必编写 boilerplate/glue 代码,您需要反射和具体化支持以及辅助概念。
如果你这样做,它看起来像:
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 获取非类型擦除类型的位置。
None 这需要概念:
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.
A 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
.
的实现
Yakk的答案是正确的,但我觉得太复杂了。
从某种意义上说,您的要求是错误的,因为您试图获得 "free" 无法免费获得的东西:
I am trying to figure out if I can use concepts as a kind of interface for classes without requiring the overhead of a virtual table.
答案是否定的。并不是因为 virtual table 的开销是一些不必要的成本。
如果您想要一组形状来使用它们,您需要存储有关特定实例的信息。
虚拟机为你做这件事(最简单的思考方式是每个实例的隐藏枚举成员,它告诉编译器在运行时调用什么成员函数),如果你想你可以手动做,但你必须以某种方式做(例如,您可以使用 std::variant<Square,Rectangle>
)。
如果不这样做,指向 Shapes 的指针数组与指向 void 的指针数组一样好。你不知道你的指针指向什么。
注意:如果您真的因为虚拟开销而在性能上苦苦挣扎,请考虑使用 Boost polly_collection
我想弄清楚我是否可以使用概念作为 classes 的一种接口,而不需要虚拟 table 的开销。我整理了一个可以正常工作的示例,但我必须将我的 class 实例存储在一个数组中,该数组由它们的共同继承而不是它们的共同概念定义。我没有在帖子中看到任何关于概念数组的讨论,但 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;
};
我明白你想做什么,但这对你的用例没有意义。概念是在编译时强制执行接口的方法,通常用于模板函数。您在这里想要的是一个抽象接口 - 一个带有一些纯虚拟成员函数的基础 class。
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
}
附带一提,也许这只是一个人为的例子,但正方形肯定不是面向对象意义上的矩形。一个简单的例子是,有人可以在矩形基础 class 上包含一个名为 stretch
的方法,而你必须在你的正方形中实现它。当然,一个正方形在任何维度上一拉伸,它就不再是正方形了。小心。
这做不到。
我的意思是您可以实现自己的类型擦除来替换 virtusl 函数表。在您的特定情况下,它可能比 vtable 性能更高,因为您可以针对您的具体问题对其进行定制。
要从编译器获得帮助,这样您就不必编写 boilerplate/glue 代码,您需要反射和具体化支持以及辅助概念。
如果你这样做,它看起来像:
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 获取非类型擦除类型的位置。
None 这需要概念:
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.
A 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
.
Yakk的答案是正确的,但我觉得太复杂了。 从某种意义上说,您的要求是错误的,因为您试图获得 "free" 无法免费获得的东西:
I am trying to figure out if I can use concepts as a kind of interface for classes without requiring the overhead of a virtual table.
答案是否定的。并不是因为 virtual table 的开销是一些不必要的成本。
如果您想要一组形状来使用它们,您需要存储有关特定实例的信息。
虚拟机为你做这件事(最简单的思考方式是每个实例的隐藏枚举成员,它告诉编译器在运行时调用什么成员函数),如果你想你可以手动做,但你必须以某种方式做(例如,您可以使用 std::variant<Square,Rectangle>
)。
如果不这样做,指向 Shapes 的指针数组与指向 void 的指针数组一样好。你不知道你的指针指向什么。
注意:如果您真的因为虚拟开销而在性能上苦苦挣扎,请考虑使用 Boost polly_collection