6

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;
};
M.M
  • 138,810
  • 21
  • 208
  • 365
Moe42
  • 63
  • 3
  • Why raw pointer? Why built-in array? So old school for c++17 – Milo Lu Aug 24 '18 at 03:01
  • `IShape` is not a base class of `Rectangle` so I am not sure what you expect that line to do – M.M Aug 24 '18 at 03:32
  • I think `IShape *` is ill-formed in c++20 concepts , not sure why gcc6 concepts allow it to even exist – M.M Aug 24 '18 at 03:56
  • @Milo Sorry for the anachronisms. I was only trying to prove out the use of the IShape concept, not to actually provide a well-structured modern program. – Moe42 Aug 24 '18 at 22:57
  • @M.M As I mentioned, removing the inheritance was my end goal. The concept keyword was already implemented in the definition of IShape, and I was aware that concepts were not part of the official c++17 standard, but g++ 6.3.0 includes them as part of it's definition (which is why I mentioned the compiler version). – Moe42 Aug 24 '18 at 23:01
  • I followed through on @Yakk 's tuple suggestion and found the solution that I appended to the original post that works well enough for what I was trying to prove. Thanks! – Moe42 Aug 24 '18 at 23:03

3 Answers3

4

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

I see what you are trying to do, but it doesn't make sense for your use-case. Concepts are ways of enforcing an interface at compile-time, usually for template functions. What you want here is an abstract interface - a base class with a few pure virtual member functions.

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
}

An abstract interface enforces an interface at run-time - you don't know directly what the concrete type is, but you can work with the provided methods.

bool collide(ShapeInterface& s, ShapeInterface& u)
{
    // concrete types of S and U are unknown
    // only methods of interfaces are available
}

On a side-note, maybe this was just a contrived example, but a Square is certainly not a Rectangle in the Object Oriented sense. One simple example is, someone could include a method called stretch on the rectangle base class, and you have to implement it in your square. Of course, as soon as you stretch a square in any dimension, it is no longer a square. Be careful.

Alexander Kondratskiy
  • 4,156
  • 2
  • 30
  • 51
  • A readonly square is a readonly rectangle. A writeonly rectangle is a writeonly square. Well sorta. – Yakk - Adam Nevraumont Aug 24 '18 at 04:00
  • @Alexander True, it's not a geometrically correct use of square and rectangle. Just a contrived example to work with c++ concepts. As I mentioned earlier, I was trying to avoid the abstract interface because I wanted to do away with the virtual table. The solution I updated in the original post accomplished this exactly. – Moe42 Aug 24 '18 at 23:17
0

Yakk answer is correct, but I feel it is too complicated. Your requirements are wrong in a sense that you are trying to get for "free" something you can not get for 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.

Answer is no. And it is not because the overhead of virtual table is some unnecessary cost. If you want to have a array of Shapes to use them you need to store info about specific instances. Virtual machinery does this for you(simplest way to think about this is a hidden enum member for each instance that tells compiler at runtime what member functions to call), and if you want you could do it manually, but you have to do it somehow(for example you could use std::variant<Square,Rectangle>).

If you do not do it array of pointers to Shapes is as good as an array of pointers to void. You do not know what your pointers points to.

note: if you are really struggling with performance due to virtual overhead consider using Boost polly_collection

NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
  • @NoSennseEtAl I can see your point about being like a void* collection when the types are unknown. The tuple solution Yakk suggested addresses that concern (since it is a collection of non-uniform types) and provides a solution for my strange requirements (since I can use std::apply() giving it a input parameter that matches the concept). In my case, I am using g++ in a very tightly restrained binary allowance, so boost is not an option. Also, g++ 6.3.0 is another restriction for now which does not have std::variant defined yet. (I believe it is included in a later version of g++.) – Moe42 Aug 26 '18 at 07:01
  • @Moe42 tuple is not the same thing as your array of pointers. It requires you to have a fixed size, you can not reassign to 45th element Rectangle if the type at index 45 is Square... – NoSenseEtAl Aug 26 '18 at 14:36