Упрощение const Перегрузка? - PullRequest
41 голосов
/ 04 января 2011

Я преподаю класс программирования C ++ уже много лет, и одна из самых хитрых вещей, которые нужно объяснить студентам, - это перегружать их.Я обычно использую пример вектороподобного класса и его функции operator[]:

template <typename T> class Vector {
public:
    T& operator[] (size_t index);
    const T& operator[] (size_t index) const;
};

У меня практически нет проблем с объяснением, почему необходимы две версии функции operator[], нопытаясь объяснить, как объединить две реализации вместе, я часто трачу много времени на изучение языка.Проблема в том, что единственный хороший и надежный способ, которым я знаю, как реализовать одну из этих функций в терминах другой, - это трюк const_cast / static_cast:

template <typename T> const T& Vector<T>::operator[] (size_t index) const {
     /* ... your implementation here ... */
}
template <typename T> T& Vector<T>::operator[] (size_t index) {
    return const_cast<T&>(static_cast<const Vector&>(*this)[index]);
}

Проблема с этимустановка состоит в том, что это чрезвычайно сложно объяснить и совсем не интуитивно очевидно.Когда вы объясняете это как «приведение к const, затем вызываете версию const, а затем отбрасываете constness», это немного легче понять, но фактический синтаксис пугающий.Объяснение, что такое const_cast, почему это уместно здесь, и почему это почти повсеместно неуместно в других местах, обычно занимает от пяти до десяти минут лекционного времени, и для понимания всего этого выражения часто требуется больше усилий, чем от разницы между const T* и T* const.Я чувствую, что студентам нужно знать о константной перегрузке и о том, как это сделать, не дублируя код в двух функциях без необходимости, но этот трюк кажется немного чрезмерным во вводном курсе по программированию на C ++.

У меня такой вопрос- есть ли более простой способ реализации перегруженных const функций в терминах друг друга?Или есть более простой способ объяснить этот существующий трюк студентам?

Ответы [ 7 ]

11 голосов
/ 04 января 2011

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

В вашей способности преподавать C ++ я бы чувствовал себя еще сильнее в этом подходе.

8 голосов
/ 04 января 2011

Как насчет того, чтобы просто разбить его на более мелкие шаги?

const Vector<T>& const_this = *this;
const T& const_elem = const_this[index];
T& mutable_elem = const_cast<T&>(const_elem);
return mutable_elem;

Таким способом можно даже исключить static_cast, хотя вы можете оставить его, если считаете, что он будет более понятным.

7 голосов
/ 04 января 2011

Это довольно странный вариант, но это можно сделать с помощью статического помощника шаблона, как это

// template parameters can be const or non-const
template<class Ret, class C>
static Ret& get(C* p, size_t index) { /* common code here like p->array[index] */ }

Тогда вы можете написать

const T& operator[](size_t index) const { return get<const T>(this, index); }
T& operator[](size_t index) { return get<T>(this, index); }

Этот трюк избегает приведений (!) И двойной реализации, но, опять же, мне это кажется странным:)

И небольшое замечание о вашем фрагменте кода, не будет ли const_cast достаточно вместо этого static_cast, или я что-то упустил?

6 голосов
/ 04 января 2011

Вы можете удалить одно приведение, используя приватный метод:
Он добавляет метод, но делает приведение менее сложным:

template <typename T>
class Vector
{
  public:
    T const& operator[](size_t i) const { return getValue(i);}
    T&       operator[](size_t i)       { return const_cast<T&>(getValue(i));}

  private:
    T const& getValue(size_t i) const   { return /* STUFF */;}
};
3 голосов
/ 04 января 2011

На мой взгляд, это просто глупо. Вы заставляете одно реализовываться в терминах другого просто ради этого, а не потому, что полученный код легче поддерживать или понимать. Причина, по которой ваши студенты смущены, вероятно, в том, что они ДОЛЖНЫ быть.

Не каждый принцип должен быть сведен к исключительной крайности. Иногда быть избыточным просто лучше.

2 голосов
/ 04 января 2011

Если реализация будет точно таким же кодом (для этого примера «векторного» класса или чего-то еще), почему бы не неконстантной версии вызывать константную версию, а не наоборот?Если по какой-то причине код должен модифицировать элемент, то, возможно, на самом деле не должно быть константной версии (игнорируя всю изменчивую вещь ...).

2 голосов
/ 04 января 2011

Вместо того, чтобы одна версия вызывала другую, вы могли бы позволить обеим вызывать вспомогательную функцию, которая находит нужный элемент. Кажется, вы уже вводите шаблоны, поэтому если вспомогательная функция также будет шаблоном, она должна работать, избегать дублирования кода и работать как с постоянными, так и с неконстантными без const_cast s.

С другой стороны, вы можете использовать локальные переменные, чтобы помочь разбить выражение на управляемые части (с комментариями для каждой).

const ClassType& cthis = *this; // look, no explicit cast needed here :)
const T& elem = cthis[index]; // delegate to const version
return const_cast<T&>(elem); // ok to strip off the const, since we added it in the first place
...