Многоликий const

    Ключевое слово const — одно из самых многозначных в C++. Правильно использование const позволяет организовать множество проверок ещё на этапе компиляции и избежать многих ошибок из числа тех, которые бывает трудно найти при помощи отладчиков и/или анализа кода.

    Первая половина заметки рассчитана скорее на начинающих (надеюсь мнемоническое правило поможет вам запомнить, где и для чего используется const), но, возможно, и опытные программисты смогут почерпнуть интересную информацию о перегрузке методов по const.

    Константы и данные


    Самый простой случай — константные данные. Возможно несколько вариантов записи:

    const int i(1);
    int const j(1);
    int const k=1;

    Все они правильные и делают одно и тоже — создают переменную, значение которой изменить нельзя.

    const int k=1;
    k = 7; // <-- ошибка на этапе компиляции!

    Константы и указатели


    При использовании const с указателями, действие модификатора может распространяться либо на значение указателя, либо на данные на которые указывает указатель.

    Работает (const относится к данным):

    const char * a = "a";
    a="b";

    Тоже самое и тоже работает:

    char const * a = "a";
    a="b";

    А вот это уже не работает:

    char * const a = "a";
    a="b"; // <-- не работает

    Если бы операция присвоения изменяла бы не указатель, а данные:

    *a = 'Y';

    то ситуация была бы диаметрально противоположной.

    Существует мнемоническое правило, позволяющее легко запомнить, к чему относится const. Надо провести черту через "*", если const слева, то оно относится к значению данных; если справа — к значению указателя.

    Ну и конечно, const можно написать дважды:

    const char * const s = "data";

    Константы и аргументы/результаты функций


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

    Константы и методы (перегрузка)


    А вот с методами есть одна тонкость.

    Во-первых, для методов допустимо использование const, применительно к this. Синтаксис таков:

    class A {
    private:
      int x;
    public:
      void f(int a) const {
        x = a; // <-- не работает
      }
    };

    Кроме того, этот const позволяет перегружать методы. Таким образом, вы можете писать оптимизированные варианты методов для константных объектов.

    Поясняю:

    class A {
    private:
      int x;
    public:
      A(int a) {
        x = a;
        cout << "A(int) // x=" << x << endl;
      }
      void f() {
        cout << "f() // x=" << x << endl;
      }
      void f() const {
        cout << "f() const // x=" << x << endl;
      }
    };
    int main() {
      A a1(1);
      a1.f();
      A const a2(2);
      a2.f();
      return 0;
    }

    Результат:

    A(int) // x=1
    f() // x=1
    A(int) // x=2
    f() const // x=2

    То есть для константного объекта (с x=2) был вызван соответствующий метод.

    Осталось только добавить, что если вы планируете использовать const-объекты, то вам надо обязательно реализовать const-методы. Если вы в этом случае не реализуете не-const-методы, то во всех случаях будут молча использоваться const-методы. Одним словом, const лучше использовать там, где это возможно.



    Картинка взята отсюда http://museum.dp.ua/affiliate/hmuseum/idols.html

    И ещё… я собрался в отпуск… возможно, не смогу ответить на комментарии до понедельника-вторника. Не сочтите за невнимание :-)
    Поделиться публикацией

    Комментарии 34

      +2
      Очень напрягает, когда в библиотеках по сути const-методы таковыми не объявлены. Мешает реализовывать собственную const-логику. Например:
      
      // Библиотека
      class Container
      {
      public:
          unsigned count (); // не const
      };
      
      // MyModule.cpp
      class MyContainer : public Container
      {
      public:
          void process () const 
          { 
              for (unsigned i = 0; i < count (); ++i)  // Нельзя использовать не-const count()
              {...} 
          }   
      };
      


      А если объявить MyContainer::process() как не-const, то его уж нельзя будет использовать в других const-методах. И никак этот не-const count() не завернёшь в const-метод. Конечно, можно попытаться реализовать собственный метод вроде myCount() и объявить его как const, но это, конечно, не всегда возможно.

      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Что же делать, если хочется последовательно, а библиотека вот не даёт?
            +5
            Это обходиться прри помощи адартера обьекта.

            В вашем случае:

            // Вообщето православный адартер обьекта должен работать с ссылкой или указателем на адаптируемый обьект, ну да ладно
            class ContainerAdapter
            {
            public:
            unsigned count () const {return cont_.count ()}
            private:
            mutable Container cont_;
            };

            class MyContainer: public ContainerAdapter
            {
            //…
            };
              0
              Действительно, сам не додумался :) Спасибо!
                0
                Можно проще

                // Библиотека
                class Container
                {
                public:
                unsigned count (); // не const
                };

                // MyModule.cpp
                class MyContainer: protected Container
                {
                public:
                unsigned count() const
                {
                return Container::count();
                }

                // Ну и еще что нужно, соответственно, открываем
                };
                  0
                  Не даст вызвать не-const Container::count() из Container::count() const.

                  Есть вот такой вариант:

                  
                  // Библиотека
                  class Container
                  {
                  public:
                      unsigned count (); // не const
                  };
                  
                  // MyModule.cpp
                  class MyContainer : public Container
                  {
                  public:
                      void process () const 
                      { 
                          for (unsigned i = 0; 
                              i < (const_cast<MyContainer*>(this))->count (); // Приводим this к не-const типу
                              ++i)  
                          {...} 
                      }   
                  };
                  
                  
                    0
                    Тогда уж вот так:
                    ((Container*)(const_cast<MyContainer*>(this)))->count()

                    А вообще, убивая вот так просто слово const, Вы нарушаете гарантию, данную этим словом, что «ни один из членов класса не претерпит каких-либо изменений».
                      +2
                      1. Зачем преобразование к Container*?
                      2. Согласен насчёт нарушения гарантий, но что же делать, если библиотека не даёт гарантий там, где должна бы? Конечно, не однозначный вопрос, что должна библиотека, а что нет. Однако, const-корректность библиотеки делает её более удобной. Короче говоря, лучше уж аккуратно сделать const_cast в одном изолированном месте, чем отказываться от ключевого слова const вообще.
                        0
                        1. Поменяйте название process на count и сразу поймете зачем. Это преобразование, в конечном счете, не сделает код более медленным или громоздким, а вот избежать ошибок и ввести лишнюю ясность поможет.
                        2. Я тоже предпочел бы сделать так, хоть и считаю это менее правильным. Дилемма :)
                          +2
                          1. В таком случае нужно сделать так:
                          const_cast<MyContainer*>(this))->Container::count ()

                          поскольку преобразование типа к базовому классу не отменяет полиморфизма.
                        0
                        В дополнение к вышесказанному — не используйте c-style cast.
                      +3
                      Это тоже самое, что Blackened привел в самом нечале. Нельзя вызвать не-конст ф-цию из конст.
                    0
                    Сделать const_cast на this. Не очень красиво, но работает.
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        Не поверите, но вы — первый, кто спрашивает. И — таки да, вы угадали. :)
                0
                класс. не писал на c++ со времен учебы.
                но все равно интересно!
                • НЛО прилетело и опубликовало эту надпись здесь
                    +2
                    Я просто думаю, что статья на хабре хорошо читается, если её объём 3-5К; mutable и const_cast не вписались. Можно развить тему в статье «const и его друзья» :-)
                    0
                    Такую фотку надо вешать на книги, типа «С++ для чайников»))))
                      +3
                      Прописная истина же.
                      И, я дак, например, черту не провожу, читаю как есть, так же как в случае с массивом указателей (int *a[]) или указателем на массив (int (*a)[]).

                      const char * a = «1»;
                      1) идентификатор а объявлен как
                      2) указатель на
                      3) значение const char.

                      char * const a = «1»;
                      1) идентификатор а объявлен как
                      2) const указатель на
                      3) значение char.
                        0
                        а еще можно делать так

                        class A
                        {
                        void f(int);
                        };

                        void A::f(const int);

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

                        Далее.
                        Если вы хотите объявить const поле класса, то инициализировать его обязательно списком инициализации ( так же как и ссылки )

                        class B
                        {
                        const int constValue;
                        public:
                        B(int&);
                        };

                        B::B()
                        :constValue(33)
                        {}

                        в противном случае если вы попробете сделать это в теле конструктора — получите ошибку.
                        В данном случае случше лучше всего руководствоваться следующим примером

                        аналог инициализации в теле конструктора ( между {} )
                        int x;
                        x=5;

                        аналог инициализации списоком инициализации ( после :)
                        int x = 5;

                        именно поэтому для инициализации констант и ссылок в качестве полей класса подходит только список инициализации
                          0
                          Тут нужно заметить, что члены класса лучше инициализировать в списке инициализации, в не зависимости от того, const они или нет.
                          0
                          ...«Если вы в этом случае не реализуете не-const-методы, то во всех случаях будут молча использоваться const-методы.»…

                          непонятно…
                            0
                            Я честно пытался написать понятней, но не смог. Для пояснения написан пример.
                            А вообще, тут уже обсуждение интересней, чем статья :-)
                            0
                            > const int i(1);
                            > int const j(1);
                            >
                            > Все они правильные и делают одно и тоже — создают переменную,
                            > значение которой изменить нельзя.

                            Что это за переменные такие i(1) и j(1)? Просветите плиз, что это означает.
                              0
                              Переменные называются i и j, а int i(1); это определение переменной i типа int и инициализация её значением 1. Читайте 8.5/1.
                                0
                                Мда ну и синтаксис…

                                Зачем стандатр позволяет такие запутывающие конструкции? Ведь по синтаксису написание аналогично вызову функции.
                                  0
                                  class A
                                  {
                                  public:
                                      A(int i) {}
                                  };
                                  
                                  class B
                                  {
                                  public:
                                      B(int i, int j) {}
                                  };
                                  
                                  int main()
                                  {
                                      A a1 = 1;
                                      A a2(2);
                                  
                                      int i1 = 1;
                                      int i2(2);
                                  
                                      B b1(1, 2);
                                  }


                                  Я доступно объяснил, или нужно прокомментировать?
                                    0
                                    Честнагря нихрена не понял. Тем более не увидел ответа на вопрос — зачем логическую конструкцию с присваиванием писать через int i2(2) вместо int i2=2?
                                      0
                                      Объясняю.

                                      По идеологии С++ пользовательские типы не должны в использовании отличаться от встроенных. Для встроенных типов разрешена инициализация вида Type t = Initializer;, поэтому такую же инициализацию разрешили для пользовательских типов, если есть конструктор, принимающий один аргумент (см. класс A). Для пользовательских типов инициализация вида Type t(InitializerList); универсальна (см. классы A и B), поэтому для однообразности есть возможность инициализировать точно так же и экземпляры встроенных типов.
                                        0
                                        Теперь понятно. Позволяла бы карма — плюсанул.
                                0
                                Это инициализация этих переменных единицами. То же самое, что и const int i = 1; int const j = 1;
                                0
                                >Существует мнемоническое правило, позволяющее легко запомнить, к чему относится const. Надо провести черту через "*", если const слева, то оно относится к значению данных; если справа — к значению указателя.

                                Еще лучше мнемоническое правило было написано у Бочкова и Субботина: «изнутри наружу». Все, что участвует в создании типа объявляемой сущности: const, volatile, *, (), [] читается от имени этой сущности (самая внутренность) — наружу (то есть влево для префиксов и вправо для постфиксов). Постфиксы () [] имют приоритет перед префиксами const volatile *. Приоритет может быть изменен скобками.

                                В частности:
                                char const *pch;
                                      (2) (1)(0)
                                     <------------
                                

                                pch(0) — это указатель(1) на const(2)

                                char* const cpch;
                                   (2) (1)  (0)
                                     <------------
                                

                                pch(0) — это константный(1) указатель(2)

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое