【问题标题】:Array of concept pointers概念指针数组
【发布时间】:2018-08-24 02:56:29
【问题描述】:

我试图弄清楚是否可以将概念用作类的一种接口,而无需虚拟表的开销。我整理了一个可以工作的示例,但是我必须将我的类实例存储在由它们的共同继承而不是它们的共同概念定义的数组中。我在帖子中没有看到任何关于概念数组的讨论,但 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;
};

【问题讨论】:

  • 为什么是原始指针?为什么要内置数组? c++17 的老派
  • IShape 不是Rectangle 的基类,所以我不确定您希望该行做什么
  • 我认为 IShape * 在 c++20 概念中的格式不正确,不知道为什么 gcc6 概念允许它存在
  • @Milo 对不合时宜的情况感到抱歉。我只是想证明 IShape 概念的用途,而不是真正提供结构良好的现代程序。
  • @M.M 正如我所提到的,删除继承是我的最终目标。在 IShape 的定义中已经实现了 concept 关键字,并且我知道概念不是官方 c++17 标准的一部分,但 g++ 6.3.0 将它们作为定义的一部分包含在内(这就是我提到编译器的原因版本)。

标签: c++ arrays c++-concepts c++20 gcc6


【解决方案1】:

这是做不到的。

我的意思是您可以实现自己的类型擦除来替换 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";
    }
});

以上可以写成

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) );
}

中,lambda 中的行改为:

    using discard=int[];
    (void)discard{ 0,((void)f(decltype(args)(args)),0)... };

这有点迟钝,需要实现std::apply

中,您必须在外部编写一个模仿 lambda 的结构。

【讨论】:

    【解决方案2】:

    我知道您要做什么,但这对您的用例没有意义。概念是在编译时强制执行接口的方法,通常用于模板函数。你需要的是一个抽象接口——一个带有几个纯虚成员函数的基类。

    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 的方法,并且您必须在您的正方形中实现它。当然,只要你在任何维度上拉伸一个正方形,它就不再是一个正方形。小心点。

    【讨论】:

    • 只读正方形是只读矩形。只写矩形是只写正方形。好吧。
    • @Alexander 没错,这不是正方形和矩形的几何正确使用。只是一个使用 c++ 概念的人为示例。正如我之前提到的,我试图避免使用抽象接口,因为我想取消虚拟表。我在原帖中更新的解决方案正是完成了这一点。
    【解决方案3】:

    Yakk 的答案是正确的,但我觉得它太复杂了。 从某种意义上说,您的要求是错误的,因为您试图“免费”获得您无法免费获得的东西:

    我试图弄清楚是否可以将概念用作类的一种接口,而无需虚拟表的开销。

    答案是否定的。并不是因为虚拟表的开销是一些不必要的成本。 如果您想拥有一组 Shapes 来使用它们,您需要存储有关特定实例的信息。 虚拟机为您执行此操作(考虑此问题的最简单方法是每个实例的隐藏枚举成员,它在运行时告诉编译器要调用哪些成员函数),如果您愿意,您可以手动执行此操作,但您必须以某种方式执行此操作(例如你可以使用std::variant&lt;Square,Rectangle&gt;)。

    如果你不这样做,指向 Shapes 的指针数组与指向 void 的指针数组一样好。你不知道你的指针指向什么。

    注意:如果您真的因为虚拟开销而在性能方面遇到困难,请考虑使用 Boost polly_collection

    【讨论】:

    • @NoSennseEtAl 我可以理解你的观点,即当类型未知时,它就像一个 void* 集合。 Yakk 建议的元组解决方案解决了这个问题(因为它是非统一类型的集合)并为我的奇怪需求提供了解决方案(因为我可以使用 std::apply() 给它一个与概念匹配的输入参数)。就我而言,我在非常严格限制的二进制余量中使用 g++,因此不能选择 boost。此外,g++ 6.3.0 是目前尚未定义 std::variant 的另一个限制。 (我相信它包含在更高版本的 g++ 中。)
    • @Moe42 元组与您的指针数组不同。它要求您具有固定大小,如果索引 45 处的类型是 Square,则不能重新分配给第 45 个元素 Rectangle...
    猜你喜欢
    • 1970-01-01
    • 2014-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多