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;
};
Declaration of an Array of Pointers in C In simple words, this array is capable of holding the addresses a total of 55 integer variables. Think of it like this- the ary[0] will hold the address of one integer variable, then the ary[1] will hold the address of the other integer variable, and so on.
Arrays are commonly used in computer programs to organize data so that a related set of values can be easily sorted or searched. For example, a search engine may use an array to store Web pages found in a search performed by the user.
The pointer amount is initialized to point to total : double time, speed, *amount = &total; The compiler converts an unsubscripted array name to a pointer to the first element in the array. You can assign the address of the first element of an array to a pointer by specifying the name of the array.
An array is a collection of elements of the same type placed in contiguous memory locations that can be individually referenced by using an index to a unique identifier. Five values of type int can be declared as an array without having to declare five different variables (each with its own identifier).
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 c++14.
A c++17 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 c++14 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 c++11 you'd have to write a struct outside that mimics the c++14 lambda.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With