Я пытаюсь выяснить, могу ли я использовать концепции как своего рода интерфейс для классов, не требуя дополнительной нагрузки на виртуальную таблицу. Я собрал пример, который вроде работает, но я должен хранить экземпляры моего класса в массиве, определяемом их общим наследованием, а не их общей концепцией. Я не вижу ничего обсуждаемого в постах о массивах понятий, но 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*
массив (как в строке с комментариями ниже той, которая вызвала первую ошибку), программа компилируется и запускается, как и ожидалось.
Почему массив указателей концептов не разрешен? Будет ли это возможно в будущей версии с ++?
(Мой пример включает в себя виртуальные функции и наследование, хотя моя цель состояла в том, чтобы устранить их. Я включил их только для удобства, чтобы получить 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 для включения apply (), как определено стандартом C ++ 17, но он был доступен в std :: экспериментальный. (Я думаю, что это может быть добавлено в более поздней версии 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;
};
Это не может быть сделано.
Я имею в виду, что вы можете реализовать свое собственное стирание типов, которое заменяет таблицы функций virtusl. И это, возможно, может быть более производительным, чем vtable в вашем конкретном случае, потому что вы можете использовать его для своей конкретной задачи.
Чтобы получить помощь от компилятора, чтобы вам не приходилось писать шаблонный / клеевой код, вам понадобится поддержка рефлексии и реификации наряду с побочными концепциями.
Если бы вы сделали это, это выглядело бы так:
ShapePtr shapes[2] = {&square, &rect};
или же
ShapeValue shapes[2] = {square, rect};
Теперь это не будет делать все, что вы надеетесь на производительность; стирание типов все еще будет прыгать через указатели на функции. И для каждого объекта или просмотра хранилища накладных расходов. Однако вы можете обменять больше памяти на меньшее количество косвенных ссылок.
Стирание типа вручную здесь в основном реализует объектную модель в C, а затем оборачивает ее, чтобы она выглядела симпатично в C ++. Объектная модель C ++ по умолчанию была лишь одним из возможных подходов, и программы на C реализуют множество альтернатив.
Вы также можете сделать шаг назад и заменить массив кортежем. Кортежи могут хранить неоднородные типы, и с помощью bkt работы вы можете перебирать их:
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";
}
});
где лямбда получает нетипизированный тип.
Ничто из этого не требует концепций:
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.
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 вместо этого строка в лямбде:
using discard=int[];
(void)discard{ 0,((void)f(decltype(args)(args)),0)... };
который немного более тупой, и требует реализации std::apply
,
В C ++ 11 Вы должны написать структуру за пределами, которая имитирует C ++ 14 лямбда.
Я вижу, что вы пытаетесь сделать, но это не имеет смысла для вашего варианта использования. Концепции — это способы реализации интерфейса во время компиляции, обычно для шаблонных функций. Здесь вам нужен абстрактный интерфейс — базовый класс с несколькими чисто виртуальными функциями-членами.
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
на базовом классе прямоугольника, и вы должны реализовать его в своем квадрате. Конечно, как только вы растягиваете квадрат в любом измерении, он больше не квадрат. Быть осторожен.
Якк ответ правильный, но я чувствую, что это слишком сложно.
Ваши требования неверны в том смысле, что вы пытаетесь получить «бесплатно» то, что вы не можете получить бесплатно:
Я пытаюсь выяснить, могу ли я использовать концепции как своего рода интерфейс для классов, не требуя дополнительной нагрузки на виртуальную таблицу.
Ответ — нет. И это не потому, что накладные расходы на виртуальную таблицу являются излишними затратами.
Если вы хотите иметь массив Shapes для их использования, вам нужно хранить информацию о конкретных экземплярах.
Виртуальная машина делает это за вас (самый простой способ думать об этом — это скрытый член enum для каждого экземпляра, который сообщает компилятору во время выполнения, какие функции-члены вызывать), и если вы хотите, вы можете сделать это вручную, но вы должны сделать это как-то (например, вы могли бы использовать std::variant<Square,Rectangle>
).
Если вы не сделаете этого, массив указателей на фигуры так же хорош, как и массив указателей на void. Вы не знаете, на что указывают ваши указатели.
примечание: если вы действительно боретесь с производительностью из-за виртуальных издержек, подумайте об использовании Boost polly_collection