Советы по более эффективному расширению контейнера STL STL с пользовательскими методами
Я унаследовал от контейнера STL С++ и добавил к нему свои собственные методы. Обоснование было таково, что для клиентов он будет выглядеть обычным списком, но имеет специфические для приложения методы, с которыми они могут быть легко вызваны.
Это отлично работает, но я прочитал множество сообщений о не наследовании от STL. Может ли кто-нибудь дать конкретный совет, как лучше написать код ниже?
class Item
{
int a;
int b;
int c;
int SpecialB()
{
return a * b + c;
}
};
class ItemList : public std::vector<Item>
{
int MaxA()
{
if( this->empty() )
throw;
int maxA = (*this)[0].a;
for( int idx = 1; idx < this->size(); idx++ )
{
if( (*this)[idx].a > maxA )
{
maxA = (*this)[idx].a;
}
}
return maxA;
}
int SpecialB()
{
if( this->empty() )
throw;
int specialB = (*this)[0].SpecialB();
for( int idx = 1; idx < this->size(); idx++ )
{
if( (*this)[idx].SpecialB() < specialB )
{
specialB -= (*this)[idx].c;
}
}
return specialB;
}
int AvgC()
{
if( this->empty() )
throw;
int cSum = 0;
for( int idx = 0; idx < this->size(); idx++ )
{
cSum += (*this)[idx].c;
}
return cSum / this->size(); // average
}
};
EDIT: Спасибо за кучу вдумчивых ответов. Я создам вспомогательные функции вместо этого и теперь не наследую от контейнеров STL.
Ответы
Ответ 1
зачем вам расширять вектор таким образом?
используйте стандартные <algorithm>
с вашими функторами.
например
std::min_element
, std::max_element
int max_a = std::max_element
(
v.begin(),
v.end(),
boost::bind(
std::less< int >(),
bind( &Item::a, _1 ),
bind( &Item::a, _2 )
)
)->a;
std::accumulate
- для расчета avarage
const double avg_c = std::accumulate( v.begin(), v.end(), double( 0 ), boost::bind( Item::c, _1 ) ) / v.size(); // ofcourse check size before divide
ваш ItemList:: SpecialB() может быть переписан как:
int accumulate_func( int start_from, int result, const Item& item )
{
if ( item.SpecialB() < start_from )
{
result -= item.SpecialB();
}
return result;
}
if ( v.empty() )
{
throw sometghing( "empty vector" );
}
const int result = std::accumulate( v.begin(), v.end(), v.front(), boost::bind( &accumulate_func, v.front(), _1, _2 ) );
BTW: если вам не нужен доступ к членам, вам не нужно наследование.
Ответ 2
Это идея bad.
Существует много причин, по которым вы не должны выходить из классов STL, прежде всего из-за того, что они не предназначены для этого. Вектор не имеет виртуального деструктора, поэтому, если вы его расширите, деструктор суперкласса не может быть вызван должным образом, и вы получите утечки памяти.
Подробнее об этом см. в этом ответе о том, почему не выводить из std::string
. Используются многие из тех же точек:
Конструктор не работает для класса, унаследованного от std::string
- Нет виртуального деструктора
- Нет защищенных функций (поэтому вы ничего не получаете путем наследования)
- Полиморфизм не будет работать, и вы получите фрагмент объекта.
std::vector
можно присваивать, но если вы добавите свои собственные поля, они не будут скопированы при назначении, если вы назначаете векторный указатель или векторную ссылку. Это связано с тем, что vector
operator=
не знает о ваших полях.
По всем этим причинам вам лучше выполнять служебные функции, чем распространять, когда речь заходит о STL.
Ответ 3
Поскольку вы можете "растягивать" вектор только с помощью своего открытого интерфейса, гораздо удобнее писать функции, которые работают с вектором, а не быть частью вектора.
Если вы планируете это хорошо, сделайте его работу с итераторами вместо индексов, и он будет работать не только с std::vector
(см. <algorithm>
для некоторых очень хороших примеров).
Например, вы можете использовать функтор для MaxA следующим образом:
struct CmpA {
bool operator()(const Item &item1, const Item &item2) { return item1.a < item2.a; }
}
const int result = std::max_element(v.begin(), v.end(), CmpA()).a;
ваш specialB может быть таким же простым с функтором и std::accumulate
EDIT: или для С++ 11 и более поздних версий, это может быть просто:
const int result = std::max_element(v.begin(), v.end(), [](const Item &item1, const Item &item2) {
return item1.a < item2.a;
}).a;
РЕДАКТИРОВАТЬ:, вы спросили, почему лучше сделать так:
если вы используете алгоритмы, шаблоны и итераторы, он будет работать, даже если вы решите поместить элементы в std::list<Item>
или что-то еще. Это просто более versitile и помогает повторное использование кода.
Плюс функции в <algorithm>
делают большую часть этого для вас, поэтому вы можете просто использовать мало функторов 3-х линейных адаптеров.
EDIT: В дополнение к этому tgamblin перечислены некоторые очень веские причины не наследовать от std::vector
(и большинства других std-контейнеров, включая std::string
).
Ответ 4
Я предпочитаю использовать stl-контейнеры в качестве детали реализации, а не как часть моего решения интерфейса. Таким образом, я могу изменить контейнеры (вектор для deque или list), если возникнет такая необходимость, без какого-либо вызывающего кода. Возможно, вам придется написать несколько функций вызова, но инкапсуляция заслуживает дополнительного ввода.
Ответ 5
Предупреждения о не наследовании от контейнеров STL появляются, потому что методы контейнеров STL не являются виртуальными. Поэтому, если вы не переопределяете методы и не нуждаетесь в полиморфном поведении, а просто расширяете класс - это нормально, чтобы наследовать контейнеры STL.
Ответ 6
Я не понимаю, почему вам нужно расширять вектор этими методами. Вы можете просто написать их как автономные функции, например:
int MaxA(const std::vector<Item>& vec) {
if(vec.empty()) {
throw;
}
int maxA = vec[0].a;
for(std::vector<Item>::const_iterator i = vec.begin(); i != vec.end(); ++i) {
if(i->a > maxA) {
maxA = i->a;
}
}
return maxA;
}
Или там std:: max_element, который будет делать то же самое... минус бросок, конечно.
Ответ 7
Если стандартные алгоритмы не имеют того, что вам нужно, просто напишите свободные функции, предпочтительно шаблонные.
Ответ 8
Вы правы, что не должны наследовать от контейнеров STL. Виртуальные функции сделают их значимо большими - базовый размер векторов будет сказываться с 12 до 16 байтов (в реализации, которую я использую). Кроме того, виртуальные функции сложно встроить, что может замедлить работу кода. Если вы обнаружите, что делаете массив из миллиона в основном пустых векторов, разница складывается довольно быстро.
Вы все равно можете получить желаемый эффект, объявив вектор как переменную-член в своем ItemList, а затем проталкивая любые методы, которые вы хотите открыть.
class ItemList {
private:
std::vector< Item > mItems;
public:
typedef std::vector< Item >::size_type size_type;
int MaxA();
int SpecialB();
Item &operator[]( size_type offset ) { return mItems[offset]; }
size_type size() const { return mItems.size(); }
};
... и так далее. Это довольно объемная работа, но она даст вам эффект, который вы просили.