5

Влияет ли использование const на параметры функции? Почему это не сказывается на сигнатуре функции?

12

Заголовок: Влияние использования const в параметрах функции и различия в декларации и определении

Здравствуйте!

У меня возник вопрос относительно использования модификатора const в функции. Рассмотрим простой мутатор, который принимает единственный булевый параметр:

void SetValue(const bool b) { my_val_ = b; }

Имеет ли использование const в данном случае какое-либо значение? Лично я стараюсь использовать его повсеместно, включая параметры, но в этом случае меня интересует, действительно ли это изменяет поведение функции или это просто вопрос стиля.

Также меня удивило, что можно опустить const в объявлении функции, но включить его в определении, например:

.h файл

void func(int n, long l);

.cpp файл

void func(const int n, const long l) { /* ... */ }

Есть ли причина для такого поведения? Мне кажется, это немного необычно, и я хотел бы понять, как это работает.

Заранее спасибо за помощь!

5 ответ(ов)

5

Ошибочно.

Дело в самодокументации вашего кода и ваших предположениях.

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

Кроме того, как кто-то уже упоминал ранее, это может немного помочь компилятору оптимизировать код (х although это маловероятно).

2

Причина в том, что const для параметра относится только к локальной области видимости внутри функции, так как она работает с копией данных. Это означает, что сигнатура функции на самом деле остается одинаковой. Однако, вероятно, это плохой стиль — делать это слишком часто.

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

1

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

Ах, как бы мне хотелось, чтобы переменные по умолчанию были const, а модификатор mutable требовался для немаркированных переменных 😃

1

Суперфлюозные const — это плохо с точки зрения API:

Добавление лишних const в код для параметров встроенных типов, передаваемых по значению, загромождает ваш API, при этом не давая никаких значимых обещаний вызывающему или пользователю API (это только затрудняет реализацию).

Слишком много const в API, когда они не нужны, похоже на "крика волка": в конечном итоге люди начнут игнорировать const, потому что он повсюду и в большинстве случаев ничего не значит.

Аргумент "редукцио ад абсурдум" к лишним const в API, подтверждающий первые два пункта, заключается в том, что если больше const — это хорошо, то каждый аргумент, к которому можно добавить const, ДОЛЖЕН иметь const. На самом деле, если это действительно так хорошо, то const должен быть по умолчанию для параметров, а ключевое слово "mutable" — только в тех случаях, когда вам нужно изменить параметр.

Попробуем добавить const куда только можно:

void mungerum(char * buffer, const char * mask, int count);

void mungerum(char * const buffer, const char * const mask, const int count);

Рассмотрим строку кода выше. Объявление не только более загромождено и длинное, но и труднее читается, при этом три из четырех ключевых слов const можно безопасно проигнорировать пользователю API. Однако избыточное использование const сделало вторую строку потенциально ОПАСНОЙ!

Почему?

Быстрый неверный взгляд на первый параметр char * const buffer может заставить вас подумать, что он не изменит память в переданном буфере данных — однако это не так! Суперфлюозные const могут привести к опасным и неверным предположениям о вашем API при быстром просмотре или прочтении.


Суперфлюозные const также плохи с точки зрения реализации кода:

#if FLEXIBLE_IMPLEMENTATION
       #define SUPERFLUOUS_CONST
#else
       #define SUPERFLUOUS_CONST             const
#endif

void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source, SUPERFLUOUS_CONST int count);

Если FLEXIBLE_IMPLEMENTATION не истинно, то API «обещает», что не реализует функцию ниже таким образом.

void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source, SUPERFLUOUS_CONST int count)
{
       // Не сработает, если !FLEXIBLE_IMPLEMENTATION
       while(count--)
       {
              *dest++=*source++;
       }
}

void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source, SUPERFLUOUS_CONST int count)
{
       for(int i=0;i<count;i++)
       {
              dest[i]=source[i];
       }
}

Это очень глупое обещание. Зачем вам создавать обещание, которое ничего не дает вашему вызывающему и только ограничивает вашу реализацию?

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

Более того, это очень поверхностное обещание, которое легко (и легально) обойти.

inline void bytecopyWrapped(char * dest,
   const char *source, int count)
{
       while(count--)
       {
              *dest++=*source++;
       }
}
void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source, SUPERFLUOUS_CONST int count)
{
    bytecopyWrapped(dest, source, count);
}

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

Эти суперфлюозные const не стоят больше, чем обещание злодея из фильма.


Но возможность лгать становится ещё хуже:

Я осознал, что вы можете несовпадать const в заголовке (объявлении) и в коде (определении) с помощью ложного const. Защитники использования const утверждают, что это хорошо, поскольку позволяет добавлять const только в определении.

// Пример использования const только в определении, а не в объявлении
struct foo { void test(int *pi); };
void foo::test(int * const pi) { }

Однако обратное также верно... вы можете добавить ложный const только в объявлении и игнорировать его в определении. Это лишь делает суперфлюозные const в API ещё более ужасной вещью и ужасной ложью — смотрите этот пример:

struct foo
{
    void test(int * const pi);
};

void foo::test(int *pi) // Смотрите, const в определении столь избыточен, что я могу проигнорировать его здесь
{
    pi++;  // Я пообещал в своём определении, что не буду изменять это
}

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

Посмотрите на этот пример. Что более читаемо? Явно ли, что единственной причиной для лишней переменной во второй функции является то, что какой-то дизайнер API вставил туда суперфлюозный const?

struct llist
{
    llist * next;
};

void walkllist(llist *plist)
{
    llist *pnext;
    while(plist)
    {
        pnext=plist->next;
        walk(plist);
        plist=pnext;    // Эта строка не скомпилируется, если plist был const
    }
}

void walkllist(llist * SUPERFLUOUS_CONST plist)
{
    llist * pnotconst=plist;
    llist *pnext;
    while(pnotconst)
    {
        pnext=pnotconst->next;
        walk(pnotconst);
        pnotconst=pnext;
    }
}

Надеюсь, мы чему-то научились. Суперфлюозные const — это ужасное пятно на API, раздражающее ограничение, поверхностное и бессмысленное обещание, ненужное препятствие и иногда приводящее к очень опасным ошибкам.

0

Следующие две строки являются функционально эквивалентными:

int foo (int a);
int foo (const int a);

Очевидно, что вы не сможете изменить a в теле функции foo, если она определена во втором виде, но снаружи нет никакой разницы.

Где const действительно оказывается полезным, так это в параметрах-ссылках или указателях:

int foo (const BigStruct &a);
int foo (const BigStruct *a);

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

*: Если, конечно, не снять модификатор const, но это уже тема для другого обсуждения.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь