c++arraysc++-conceptsc++20gcc6

Array of concept pointers


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. I put together an example which sort-of works, but I have to store my class instances in an array defined by their common inheritance rather than their common concept. I don't see anything discussed in posts about arrays of concepts, but g++ 6.3.0 does not appear to allow it. The error is:

$ 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 ) 
                         ^~~~~~

If I change the IShape* array to a Rectangle* array (as in the commented line line below the one that caused the first error), the program compiles and runs as expected.

Why is it that the array of concept pointers is not allowed? Will this likely be allowed in a future version of c++?

(My example includes virtual functions and inheritance, even though my goal was to eliminate them. I included them only as a convenience to get the Rectangle* version to work. If I can get the IShape* version to work, I plan to remove the virtual functions and the inheritance.)

Here is the code:

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

Thanks to @Yakk for the idea about using tuple. G++ 6.3.0 hadn't fully implemented the #include file to include apply() as the C++17 standard defines, but it was available in std::experimental. (I think it may be added to in a later version of g++.) Here's what I ended up with:

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

Solution

  • This can't be done.

    I mean you can implement your own type erasure that replaces virtusl function tables. And it possibly can be more performant than a vtable in your specific case, because you can taylor it for your exact problem.

    To get help from the compiler so you wouldn't have to write boilerplate/glue code, you'd need reflection and reification support along side concepts.

    If you did this, it would look like:

    ShapePtr shapes[2] = {&square, &rect};
    

    or

    ShapeValue shapes[2] = {square, rect};
    

    Now this won't do everything you hope performance wise; type erasure is still going to jump through function pointers. And have per object or view storage overhead. You can trade more storage for less indirection however.

    Manual type erasure here is basically implementing an object model in C, then wrapping it to look pretty in C++. The default C++ object model was just one possible approach, and C programs implement many alternatives.

    You could also take a step back and replace the array with a tuple. Tuples can store non-uniform types, and with a bkt of work you can iterate over them:

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

    where the lambda gets the non-type erased type.

    None of this requires concepts:

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

    the above can be written in .

    A foreach_elem looks like:

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

    in the line in the lambda is instead:

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

    which is a bit more obtuse, and requires an implementation of std::apply.

    In you'd have to write a struct outside that mimics the lambda.