Распространение константности на данные, на которые указывают переменные-члены

Новичков в C++ часто сбивает с толку тот факт, что константные функции-члены могут вызывать неконстантные методы для объектов, на которые ссылается класс (либо по указателю, либо по ссылке). Например, совершенно верно следующее:

class SomeClass
{
    class SomeClassImpl;
    SomeClassImpl * impl_; // PImpl idiom

  public:    

    void const_method() const;
};

struct SomeClass::SomeClassImpl
{
    void non_const_method() { /*modify data*/ }
};

void SomeClass::const_method() const
{
    impl_->non_const_method(); //ok because impl_ is const, not *impl_
};

Однако иногда было бы довольно удобно, если бы константность распространялась на заостренные объекты (я добровольно использовал идиому PImpl, потому что это один из случаев, когда я думаю, что «распространение константности» было бы очень полезным).

При использовании указателей этого легко добиться, используя какой-нибудь интеллектуальный указатель с операторами, перегруженными константностью:

template < typename T >
class const_propagating_ptr
{
  public:

    const_propagating_ptr( T * ptr ) : ptr_( ptr ) {}

    T       & operator*()       { return *ptr_; }
    T const & operator*() const { return *ptr_; }

    T       * operator->()       { return ptr_; }
    T const * operator->() const { return ptr_; }

    // assignment operator (?), get() method (?), reset() method (?)
    // ...

  private:

    T * ptr_;
};

Теперь мне просто нужно изменить SomeClass::impl_ на const_propagating_ptr<SomeClassImpl>, чтобы получить желаемое поведение.

Поэтому у меня есть несколько вопросов по этому поводу:

  1. Есть ли какие-то проблемы с распространением константности, которые я упустил из виду?
  2. Если нет, существуют ли какие-либо библиотеки, которые предоставляют классы для распространения константности?
  3. Разве не было бы полезно, чтобы общие интеллектуальные указатели (unique_ptr, shared_ptr и т. д.) предоставляли какое-то средство для получения такого поведения (например, через параметр шаблона)?

person Luc Touraille    schedule 18.01.2011    source источник
comment
Что, если я просто скопирую смарт-указатель? Вуаля, у меня неконстантный.   -  person Cheers and hth. - Alf    schedule 19.01.2011
comment
T const * const operator->() const { return ptr_; } - наверное второй const здесь не нужен   -  person Andriy Tylychko    schedule 19.01.2011
comment
@Alf и @robin: Эскиз возможной реализации, который я дал, вероятно, усеян ошибками (несмотря на его небольшой размер :)), это не центральный вопрос. Тем не менее, ваш отзыв очень ценен! Что касается проблемы копирования, я не вижу в данный момент, как мы могли бы предотвратить это, но часто вы не можете полностью предотвратить себя от выстрела себе в ногу (например, вы всегда можете const_cast избавиться от константности, она не не означает, что const бесполезен). Что касается второго комментария, вы правы, @robin, я по ошибке сделал это, чтобы ptr_ не был...   -  person Luc Touraille    schedule 19.01.2011
comment
[продолжение] изменено вызывающей стороной, что глупо, потому что указатель возвращается по значению... Я сразу уберу эту константу.   -  person Luc Touraille    schedule 19.01.2011
comment
константность является свойством ссылки, а не объекта, на который ссылаются.   -  person Dan D.    schedule 19.01.2011
comment
Теперь есть официальное предложение propagate_const для С++17   -  person M.M    schedule 26.02.2015


Ответы (4)


  1. Как отметил @Alf P. Steinbach, вы наблюдали за тем, что копирование вашего указателя приведет к получению неконстантного объекта, указывающего на тот же базовый объект. Pimpl (ниже) прекрасно обходит проблему, выполняя глубокое копирование, unique_ptr обходит ее, не допуская копирования. Конечно, гораздо проще, если указатель принадлежит одному лицу.

  2. Boost.Optional распространяет const -ness, однако это не совсем указатель (хотя он моделирует концепцию OptionalPointee). Я не знаю другой такой библиотеки.

  3. Я бы предпочел, чтобы они предоставили его по умолчанию. Добавление еще одного параметра шаблона (я думаю, это класс признаков) не стоит заморачиваться. Однако это радикально изменило бы синтаксис классического указателя, поэтому я не уверен, что люди будут готовы его принять.


Код класса Pimpl

template <class T>
class Pimpl
{
public:
  /**
   * Types
   */
  typedef T value;
  typedef const T const_value;
  typedef T* pointer;
  typedef const T* const_pointer;
  typedef T& reference;
  typedef const T& const_reference;

  /**
   * Gang of Four
   */
  Pimpl() : _value(new T()) {}
  explicit Pimpl(const_reference v) : _value(new T(v)) {}

  Pimpl(const Pimpl& rhs) : _value(new T(*(rhs._value))) {}

  Pimpl& operator=(const Pimpl& rhs)
  {
    Pimpl tmp(rhs);
    swap(tmp);
    return *this;
  } // operator=

  ~Pimpl() { boost::checked_delete(_value); }

  void swap(Pimpl& rhs)
  {
    pointer temp(rhs._value);
    rhs._value = _value;
    _value = temp;
  } // swap

  /**
   * Data access
   */
  pointer get() { return _value; }
  const_pointer get() const { return _value; }

  reference operator*() { return *_value; }
  const_reference operator*() const { return *_value; }

  pointer operator->() { return _value; }
  const_pointer operator->() const { return _value; }

private:
  pointer _value;
}; // class Pimpl<T>

// Swap
template <class T>
void swap(Pimpl<T>& lhs, Pimpl<T>& rhs) { lhs.swap(rhs); }

// Not to be used with pointers or references
template <class T> class Pimpl<T*> {};
template <class T> class Pimpl<T&> {};
person Matthieu M.    schedule 19.01.2011

Один подход состоит в том, чтобы просто не использовать указатель напрямую, кроме как через две функции доступа.

class SomeClass
{
  private:
    class SomeClassImpl;
    SomeClassImpl * impl_; // PImpl idiom - don't use me directly!

    SomeClassImpl * mutable_impl() { return impl_; }
    const SomeClassImpl * impl() const { return impl_; }

  public:    

    void const_method() const
    {
      //Can't use mutable_impl here.
      impl()->const_method();
    }
    void non_const_method() const
    {
      //Here I can use mutable_impl
      mutable_impl()->non_const_method();
    }
};
person Michael Anderson    schedule 19.01.2011
comment
Это решение, которое я изначально пробовал, за исключением того, что я использовал экземпляр вложенного класса для инкапсуляции указателя, чтобы предотвратить доступ к нему из внешнего класса (только геттеры имели доступ благодаря объявлениям друзей). Однако это решение добавляет много стандартного кода для каждого указателя, для которого нам нужно распространение const. Более того, ИМХО, использование геттеров и сеттеров снижает читабельность (свойства — одна из моих любимых фич C# :)!). - person Luc Touraille; 19.01.2011

Для справки: я только что узнал, что библиотека Loki действительно предоставляет константный распространяющийся указатель (ConstPropPtr<T>). Он выглядит так же, как и в вопросе, за исключением того, что он также удаляет обернутый указатель в своем деструкторе и используется для реализации Pimpl class аналогично предложенный @Matthieu (но не копируемый).

person Luc Touraille    schedule 27.09.2011

Если вы считаете, что он должен «распространять» константность, то это означает, что вы на самом деле не верите, что это указатель (или ссылка), но вы верите, что это контейнер: если значение является постоянным, когда объект является постоянным, это происходит потому, что объект содержит значение.

Таким образом, копирование объекта копирует значение, по крайней мере, логически (CoW).

Если вы настаиваете на том, что это IOW указателя/ссылки, что вы можете копировать объект при совместном использовании содержащегося значения, то у вас есть ненадежный (противоречивый) интерфейс.

Вывод: примите решение. Это либо контейнер, либо указатель.

Указатель не распространяет константность по определению.

person curiousguy    schedule 23.11.2011
comment
Я думаю, вы упустили суть: даже когда объект содержится (принадлежит) другому объекту, вам иногда нужно сохранить его с помощью указателя (полиморфизм, ленивая инициализация, брандмауэр компиляции,...). Именно в этих случаях было бы полезно распространение const: поскольку концептуально указатель содержится в объекте, константным методам объекта не должно быть позволено изменять его. Тем не менее, я согласен с тем, что мой пример класса SomeClass должен был либо определить конструктор копирования, либо запретить копирование, чтобы избежать случайного совместного использования impl_ между экземплярами SomeClass. - person Luc Touraille; 23.11.2011
comment
Я думаю, вы упустили суть: вы можете распространять const Я понимаю, и вы упустили мою мысль: вы хотите распространять константность, потому что вам не нужен указатель семантический. Пожалуйста, не называйте этот класс something_pointer! даже когда объект содержится (принадлежит) другому объекту, иногда вам нужно сохранить его с помощью указателя Да, действительно. - person curiousguy; 24.11.2011
comment
Кстати, на случай, если кто-то возразит, что имена не так важны: если вы называете что-то указателем (которое на самом деле не имеет семантики указателя), какой-нибудь другой программист может прийти и добавить связанный с указателем материал, который не совсем имеет смысл для что-то, что не должно иметь семантику указателя. Называя его контейнером, вы (в основном) избегаете этого риска. - person curiousguy; 24.11.2011
comment
Хорошо, я понял вашу точку зрения и в основном согласен. Меня немного смутило использование вами слова контейнер: оно может применяться как к охватывающему классу (SomeClass в примере), так и к оболочке, инкапсулирующей элемент данных (const_propagating_ptr можно переименовать в container), но я думал, что вы хотите применить его исходному члену данных (impl_), что не имело особого смысла. - person Luc Touraille; 24.11.2011