[Перевод] Вычислите окружность круга
«Пожалуйста, напишите на C++ функцию, которая получает диаметр круга как float и возвращает длину окружности как float».Звучит как задание на первой неделе курса по C++. Но это только на первый взгляд. Сложности возникают уже на первых этапах решения задачи. Предлагаю рассмотреть несколько подходов.
Студент: Как вам такой вариант?
#include
Сцена третья Студент: Спасибо, учитель. Честно говоря, я не знаю, что такое 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 она была исправлена.