Советы по улучшению способа расширения контейнера C ++ STL с помощью пользовательских методов - PullRequest
12 голосов
/ 25 марта 2009

Я унаследовал от контейнера C ++ 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
  }
};

РЕДАКТИРОВАТЬ : Спасибо за кучу вдумчивых ответов. Вместо этого я создам вспомогательные функции, и теперь они никогда не будут наследоваться от контейнеров STL.

Ответы [ 8 ]

20 голосов
/ 25 марта 2009

Это плохая идея.

Существует множество причин, по которым вам не следует извлекать уроки из STL, в первую очередь из-за того, что они не предназначены для этого. В Vector нет виртуального деструктора, поэтому если вы расширите его, деструктор суперкласса может быть вызван неправильно, и вы получите утечки памяти.

Подробнее об этом см. В этом ответе, почему не является производным от std::string. Применяются многие из тех же пунктов:

Конструктор не работает для класса, унаследованного от std :: string

  • Нет виртуального деструктора
  • Нет защищенных функций (так что вы ничего не получите, наследуя)
  • Полиморфизм не будет работать, и вы получите нарезку объектов. std::vector присваивается, но если вы добавите свои собственные поля, они не будут скопированы при присваивании, если вы назначите из векторного указателя или векторной ссылки. Это потому, что vector operator= не знает о ваших полях.

По всем этим причинам вам лучше создавать служебные функции, чем расширять, когда дело касается STL.

12 голосов
/ 25 марта 2009

почему вам нужно расширить вектор таким образом?

используйте стандартные <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 - для расчета среднего значения

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

Кстати: если вам не нужен доступ к участникам, вам не нужно наследование.

11 голосов
/ 25 марта 2009

Поскольку вы можете "расширять" вектор только с помощью его открытого интерфейса, гораздо полезнее писать функции, которые работают с вектором, а не являются частью вектора.

Черт, если вы все спланируете хорошо, сделайте так, чтобы он работал с итераторами, а не с индексами, и он будет работать не только с 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

РЕДАКТИРОВАТЬ: или для c ++ 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> или что-то еще. Он просто более универсален и помогает многократно использовать код.

Плюс функции из <algorithm> делают для вас многое из этого, так что вы можете просто использовать маленькие функторы с 3 линейными адаптерами.

РЕДАКТИРОВАТЬ: В дополнение к этому, tgamblin перечислил несколько очень убедительных причин не наследовать от std::vector (и большинства других контейнеров std, включая std::string).

5 голосов
/ 25 марта 2009

Я предпочитаю использовать контейнеры stl в качестве детали реализации , а не как часть интерфейса моего решения . Таким образом, я могу изменить контейнеры (вектор для deque или list), если возникнет необходимость без какого-либо воздействия на вызывающий код. Возможно, вам придется написать несколько вызовов через функции, но инкапсуляция стоит того, чтобы ее набрать.

2 голосов
/ 25 марта 2009

Предупреждения об отсутствии наследования от контейнеров STL появляются потому, что методы контейнеров STL не являются виртуальными. Поэтому, если вы не переопределяете методы и не нуждаетесь в полиморфном поведении, а просто расширяете класс - все в порядке, чтобы наследовать контейнеры STL.

1 голос
/ 25 марта 2009

Вы правы, что не должны наследовать от контейнеров 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(); }
};

... и так далее. Это довольно трудоемкая работа, но она даст вам эффект, о котором вы просили.

1 голос
/ 25 марта 2009

Если стандартные алгоритмы не имеют того, что вам нужно, просто напишите бесплатные функции, желательно шаблонные.

1 голос
/ 25 марта 2009

Я не понимаю, почему вам нужно расширить вектор этими методами. Вы можете просто написать их как отдельные функции, например:

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, который сделал бы то же самое ... минус бросок, конечно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...