Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
6.5.2. Встраиваемые функции и функции constexpr
В разделе 6.3.2 приведена небольшая функция, возвращающая ссылку на более короткую строку из двух переданных ей. К преимуществам определения функции для такой маленькой операции относятся следующие.
• Обращение к функции shorterString() проще и понятнее, чем эквивалентное условное выражение.
• Использование функции гарантирует одинаковое поведение. Она гарантирует, что каждая проверка будет выполнена тем же способом.
• Если придется внести изменение, проще сделать это в теле функции, а не выискивать в коде программы все случаи применения эквивалентного выражения.
• Функция может быть многократно использована при написании других приложений.
Однако у функции shorterString() есть один потенциальный недостаток: ее вызов происходит медленнее, чем вычисление эквивалентного выражения. На большинстве машин при вызове функции осуществляется довольно много действий: перед обращением сохраняются регистры, которые необходимо будет восстановить после выхода; происходит копирование значений аргументов; управление программой переходит к новому участку кода.
Встраиваемые функции позволяют избежать дополнительных затрат на вызовСодержимое функции, объявленной встраиваемой (inline) при компиляции, как правило, встраивается по месту вызова. Предположим, что функция shorterString() объявлена встраиваемой, а ее вызов имеет такой вид:
cout << shorterString(s1, s2) << endl;
При компиляции тело функции окажется встроено по месту вызова, и в результате получится нечто вроде следующего:
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
Таким образом, во время выполнения удастся избежать дополнительных затрат, связанных с вызовом функции shorterString().
Чтобы объявить функцию shorterString() встраиваемой, в определении, перед типом возвращаемого значения, располагают ключевое слово inline.
// встраиваемая версия функции сравнения двух строк
inline const string &
shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
Объявление функции встраиваемой является только рекомендацией компилятору. Компилятор вполне может проигнорировать эту рекомендацию.
На самом деле механизм встраивания применяется в процессе оптимизации объектного кода, в ходе которого код небольших функций, вызов которых происходит достаточно часто, встраивается по месту вызова. Большинство компиляторов не будет встраивать рекурсивные функции. Функция на 75 строк также, вероятно, не будет встроена.
Функции constexprФункция constexpr — это функция, которая может быть применена в константном выражении (см. раздел 2.4.4). Функция constexpr определяется как любая другая функция, но должна соответствовать определенным ограничениям: возвращаемый тип и тип каждого параметра должны быть литералами (см. раздел 2.4.4), тело функции должно содержать только один оператор return:
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // ok: foo - константное выражение
Здесь функция new_sz определена как constexpr, она не получает никаких аргументов. Компилятор может проверить (во время компиляции), что вызов функции new_sz() возвращает константное выражение, поэтому ее можно использовать для инициализации переменной constexpr по имени foo.
Если это возможно, компилятор заменит вызов функции constexpr ее результирующим значением. Для этого функция constexpr неявно считается встраиваемой.
Тело функции constexpr может содержать другие операторы, если они не выполняют действий во время выполнения. Например, функция constexpr может содержать пустые операторы, псевдонимы типа (см. раздел 2.5.1) и объявления using.
Функции constexpr позволено возвратить значение, которое не является константой:
// scale(arg) - константное выражение, если arg - константное выражение
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
Функция scale() возвратит константное выражение, если ее аргумент будет константным выражением, но не в противном случае:
int arr[scale(2)]; // ok: scale(2) - константное выражение
int i = 2; // i - неконстантное выражение
int a2[scale(i)]; // ошибка: scale(i) - неконстантное выражение
Если передать константное выражение (такое как литерал 2), возвращается тоже константное выражение. В данном случае компилятор заменит вызов функции scale() результирующим значением.
Если происходит вызов функции scale() с выражением, которое не является константным (например, объект i типа int), то возвращается неконстантное выражение. Если использовать функцию scale() в контексте, требующем константного выражения, компилятор проверит, является ли результат константным выражением. Если это не так, то компилятор выдаст сообщение об ошибке.
Функция constexpr не обязана возвращать константное выражение.
Помещайте встраиваемые функции и функции constexpr в файлы заголовкаВ отличие от других функций, встраиваемые функции и функции constexpr могут быть определены в программе несколько раз. В конце концов, чтобы встроить код, компилятор нуждается в определении, а не только в объявлении. Однако все определения конкретной встраиваемой функции и функции constexpr должны совпадать точно. В результате встраиваемые функции и функции constexpr обычно определяют в заголовках.
Упражнения раздела 6.5.2Упражнение 6.43. Какое из следующих объявлений и определений имеет смысл поместить в файл заголовка, а какой — в текст файла исходного кода? Объясните почему.
(a) inline bool eq(const BigInt&, const BigInt&) {...}
(b) void putValues(int *arr, int size);
Упражнение 6.44. Перепишите функцию isShorter() из раздела 6.2.2 как встраиваемую.
Упражнение 6.45. Пересмотрите функции, написанные для предыдущих упражнений, и решите, должны ли они быть определены как встраиваемые. Если да, то сделайте это. В противном случае объясните, почему они не должны быть встраиваемыми.
Упражнение 6.46. Возможно ли определить функцию isShorter как constexpr? Если да, то сделайте это. В противном случае объясните, почему нет.
6.5.3. Помощь в отладке
Для условного выполнения отладочного кода программисты С++ иногда используют подход, подобный защите заголовка (см. раздел 2.6.3). Идея в том, что программа будет содержать отладочный код, который выполняется только во время разработки программы. Когда приложение закончено и готово к выпуску, отладочный код исключается. Этот подход подразумевает использование двух средств препроцессора: assert и NDEBUG.
Макрос препроцессора assertМакрос assert — это макрос препроцессора (preprocessor macro). Макрос препроцессора — это переменная препроцессора, действующая как встраиваемая функция. Макрос assert получает одно выражение и использует его как условие:
assert(выражение);
Если результат выражения ложь (т.е. нуль), то макрос assert выдает сообщение и закрывает программу. Если результат выражения — истина (т.е. он отличен от нуля), то макрос assert не делает ничего.
Действие макроса препроцессора подобно вызову функции. Макрос assert получает одно выражение, которое он использует как условие.
Макрос assert определен в заголовке cassert. Как уже упоминалось, относящиеся к препроцессору имена обрабатывает препроцессор, а не компилятор (см. раздел 2.3.2). В результате такие имена можно использовать непосредственно, без объявления using. Таким образом, используется имя assert, а не std::assert, кроме того, для него не предоставляется объявление using.
Макрос assert зачастую используется для проверки "недопустимых" условий. Например, программа обработки вводимого текста могла бы проверять, что все вводимые слова длиннее некоего порогового значения. Эта программа могла бы содержать такой оператор:
assert(word.size() > threshold);
Переменная препроцессора NDEBUGПоведение макроса assert зависит от состояния переменной препроцессора NDEBUG. Если переменная NDEBUG определена, макрос assert ничего не делает. По умолчанию переменная NDEBUG не определена, поэтому по умолчанию макрос assert выполняет проверку.
Отладку можно "выключить", предоставив директиву #define, определяющую переменную NDEBUG. В качестве альтернативы большинство компиляторов предоставляет параметр командной строки, позволяющий определять переменные препроцессора: