«Пожалуйста, напишите на C++ функцию, которая получает диаметр круга как float и возвращает длину окружности как float».
Звучит как задание на первой неделе курса по C++. Но это только на первый взгляд. Сложности возникают уже на первых этапах решения задачи. Предлагаю рассмотреть несколько подходов.
Студент: Как вам такой вариант?
#include <math.h>
float CalcCircumference1(float d)
{
return d * M_PI;
}
Преподаватель: Да, этот код может нормально откомпилироваться. А может и нет. M_PI не определена в стандартах C или C++. С компилятором в VC++ 2005 это сработает, но для более поздних версий придется использовать #define _USE_MATH_DEFINES перед включением math.h, чтобы получить доступ к этой нестандартной константе. Причем в результате вы напишете код, с которым могут не справиться другие компиляторы.
Сцена вторая
Студент: Благодарю за мудрость, учитель. Я убрал зависимость от нестандартной константы M_PI. Так лучше?
float CalcCircumference2(float d)
{
return d * 3.14159265358979323846;
}
Преподаватель: Да, так лучше. Этот код будет скомпилирован, и вы получите искомый результат. Но ваш код неэффективен. Вы умножаете число с одинарной точностью на константу с двойной точностью. Компилятору придется привести параметр функции типа float к типу double, а затем выполнить обратное преобразование для получения возвращаемого значения. Если вы компилируете код для SSE2, то это добавляет две инструкции в цепочку зависимостей и вычисления могут выполняться втрое дольше! В большинстве случаев такие задержки вполне допустимы, но во внутреннем цикле негативный эффект может быть весьма значительным.
Если вы компилируете для платформы x87, то преобразование в тип double ничего не стоит, а вот обратное преобразование затратно – настолько затратно, что некоторые оптимизирующие компиляторы выбрасывают это преобразование, а в результате можно получить КРАЙНЕ НЕОЖИДАННЫЕ результаты – например, CalcCircumference( r ) == CalcCircumference( r ) вернет false!
Сцена третья
Студент: Спасибо, учитель. Честно говоря, я не знаю, что такое SSE2 и x87, но я вижу, насколько элегантным становится код, когда типы согласованы. Это настоящая поэзия. Я буду использовать константу одинарной точности. Как вам вот это?
float CalcCircumference3(float d)
{
return d * 3.14159265358979323846f;
}
Преподаватель: Да, превосходно! Символ «f» в конце константы все меняет. Если бы вы посмотрели на сгенерированный машинный код, вы бы поняли, что этот вариант намного компактнее и эффективнее. Однако у меня есть замечания к стилю. Не кажется ли вам, что этой загадочной константе не место внутри функции? Даже если это число Пи, значение которого вряд ли изменится, лучше присвоить константе имя и поместить в заголовочный файл.
Сцена четвертая
Студент: Спасибо. Вы объясняете все очень доходчиво. Я помещу строку кода ниже в общий файл заголовка и буду использовать ее в своей функции. Так нормально?
const float pi = 3.14159265358979323846f;
Преподаватель: Да, отлично! С помощью ключевого слова «const» вы указали, что переменная не должна и не может быть изменена, кроме того, ее теперь можно поместить в заголовочный файл. Но, боюсь, теперь нам придется углубиться в некоторые тонкости определения областей видимости в C++.
Объявив pi с ключевым словом const, вы получите в качестве бонуса эффект ключевого слова static. Для целочисленных типов это нормально, но если вы имеете дело с другим типом данных (число с плавающей точкой, массив, класс, структура), память под вашу переменную может быть выделена отдельно в каждой единице трансляции, которая включает в себя ваш заголовочный файл. В некоторых случаях у вас в итоге будет несколько десятков или даже сотен экземпляров переменной типа float и ваш исполняемый файл будет неоправданно большим.
Сцена пятая
Студент: Вы шутите? И что делать?
Преподаватель: Да, мы пока далеки от идеала. Вы можете повесить на объявление константы атрибут __declspec(selectany) или __attribute __(weak), для того чтобы VC++ и GCC, соответственно, поняли, что достаточно сохранить одну из многочисленных копий этой константы. Но поскольку мы с вами находимся в идеалистическом мире науки, я настаиваю на применении стандартных конструкций C++.
Сцена шестая
Студент: То есть примерно так? С помощью constexpr из C++11?
constexpr float pi = 3.14159265358979323846f;
Преподаватель: Да. Теперь ваш код идеален. Конечно, VS 2013 не сможет его откомпилировать, потому что не знает, что делать с constexpr. Но вы всегда можете воспользоваться набором инструментов Visual C++ Compiler Nov 2013 CTP либо последней версией GCC или Clang.
Студент: А #define можно использовать?
Преподаватель: Нет!
Студент: А, к черту все это! Лучше я стану бариста.
Сцена седьмая
Студент: Стоп, я кое-что припоминаю. Это же просто! Вот как будет выглядеть код:
mymath.h:
extern const float pi;
mymath.cpp:
extern const float pi = 3.14159265358979323846f;
Преподаватель: Точно, в большинстве случаев это будет верное решение. Но что если вы работаете над DLL, как внешние функции будут обращаться к mymath.h в вашей DLL? В таком случае вам придется обеспечить экспорт и импорт этого символа.
Проблема в том, что правила для целочисленных типов абсолютно другие. Целесообразно и рекомендуется добавить в заголовочный файл C++ следующее:
const int pi_i = 3;
Число Пи здесь указано недостаточно точно, но дело в том, что целочисленные константы в заголовочных файлах не требуют выделения памяти в отличие от остальных констант. Чем такое отличие обусловлено, не совсем понятно, но чаще всего это и не важно.
О том, что значит «static» в «const», я узнал несколько лет назад, когда меня попросили выяснить, почему одна из наших ключевых библиотек DLL вдруг прибавила в весе 2 МБ. Оказывается, в заголовочном файле был массив констант, и мы получили тридцать копий этого массива в DLL. То есть иногда это все же имеет значение.
И да, я по-прежнему считаю, что #define — ужасный выбор в данном случае. Может быть, это еще не самое худшее решение, но мне оно совершенно не нравится. Однажды я столкнулся с ошибками компиляции, вызванными объявлением pi с помощью #definei. Приятного мало, скажу я вам! Замусоривание пространства имен — вот главная причина, почему следует избегать #define, насколько это возможно.
Заключение
Не знаю точно, какой урок мы извлекли из всего этого. Суть проблемы, которая возникает, когда мы объявляем в заголовочных файлах константу типа float или double либо структуру или массив констант, ясна далеко не всем. В большинстве серьезных программ из-за этого возникают дубликаты статических констант, и иногда они неоправданно большого размера. Полагаю, constexpr может избавить нас от этой проблемы, но у меня нет достаточного опыта его использования, чтобы знать наверняка.
Я сталкивался с программами, которые были на сотни килобайт больше своего «реального» размера — и все из-за массива констант в заголовочном файле. Я также видел программу, в которой в конечном счете оказалось 50 копий объекта класса (плюс еще по 50 вызовов конструкторов и деструкторов), потому что этот объект класса был определен как тип const в заголовочном файле. Иначе говоря, тут есть над чем подумать.
Вы можете увидеть, как это происходит с GCC, загрузив тестовую программу отсюда. Соберите её с помощью команды make, а затем выполните команду objdump -d constfloat | grep flds, чтобы найти четыре инструкции чтения со смежных адресов в сегменте данных. Если вы хотите занять больше пространства, добавьте в header.h следующее:
const float sinTable[1024] = { 0.0, 0.1, };
В случае с GCC прирост составит 4 КБ на одну запись преобразования (исходный файл), то есть исполняемый файл вырастет на 20 КиБ, даже если к таблице ни разу никто не обращается.
Как обычно, операции над числами с плавающей точкой связаны со значительными трудностями, но в данном случае, как мне кажется, в этом виновата слишком медленная эволюция языка С++.
Что еще почитать по теме:
VC++: как избежать дублирования и как понять, что дублирования не избежать
У компилятора в VC++ 2013 Update 2 появился параметр /Gw, который помещает каждую глобальную переменную в отдельный контейнер COMDAT, позволяя компоновщику выявлять и избавляться от дубликатов. Иногда такой подход помогает избежать негативных последствий объявления констант и статических переменных в заголовочных файлах. В Chrome такие изменения помогли сэкономить около 600 КБ (подробности). Частично такой экономии удалось добиться (сюрприз!) путем удаления тысячи экземпляров twoPiDouble и piDouble (а также twoPiFloat и piFloat).
Однако в VC++ 2013 STL есть несколько объектов, объявленных как static или const в объявлении класса, которые /Gw не может удалить. Все эти объекты занимают по одному байту, но в итоге набегает свыше 45 килобайт. Я сообщил разработчикам об этой ошибке и получил ответ, что в VC++ 2015 она была исправлена.
Автор: denisfrolov