Comments 77
Для второго случая (и в том числе для оптимизаций) в более других языках придумали final. Либо завезли const сразу со смыслом final.
В тех контекстах, в которых final применимо, const тоже справляется.
Если искать что-то более мощное — надо смотреть в сторону контроля времени жизни и правил заимствования.
Плюс константная ссылка ловит временные объекты и не нужна перегрузка & и &&.
А самый важный фактор, имхо, зачем использовать конст в некоторых местах — это то, что константные объекты доступны только на чтение и соответственно должны быть потокобезопасными.
константные объекты доступны только на чтение и соответственно должны быть потокобезопасными
Вот только гарантий что этот объект не используется одновременно в другом месте для записи — нет.
Вы, судя по всему, никогда не работали с библиотеками, от которых у вас только интерфейс и посмотреть в реализацию не получится.
const correctness. Я тоже ни разу не использовал const
в надежде на ускорение кода. Да и вообще, иммутабельность — это очень хорошо в контексте многопоточного программирования.
#include <stdio.h>
int func(const int num){
int result = 0;
for (int i=0; i<num; ++i){
result += i;
}
return result;
}
int func2(int num){
int result = 0;
for (int i=0; i<num; ++i){
result += i;
}
return result;
}
int main()
{
printf("%d, %d\n", func(10), func2(10));
return 0;
}
выхлоп llvm
define i32 @main() local_unnamed_addr #1 {
%1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i64 0, i64 0), i32 45, i32 45)
ret i32 0
}
Хотя сами функции одинаковы. Возможно в более сложных примерах const позволяет чуть лучше оптимизировать.
Разве const не говорит компилятору помещать такие данные(инициализированные строки/массивы), объявленные как статические или глобальные, в Read-Only разделы памяти? На процессорах с Гарвардской архитектурой это очень важно.
Структуры с нетривиальным конструктором туда так просто не поместить.
И нет, const не может говорить компилятору ничего подобного, ведь стандарт не предписывает никакого единственно возможного способа генерации машинного кода, лишь устанавливает требования к поведению.
Насколько я помню, в avr-gcc (типичный пример компилятора для гарвардской архитектуры) это не так, нужно явно указывать атрибут PROGMEM.
132) The implementation may place a const object that is not volatile in a read-only region of storage. Moreover, the implementation need not allocate storage for such an object if its address is never used.
Может, но, видимо, не обязательно.
"… закончил серию изменений в коде браузера Chrome, которая уменьшила размер его бинарника под Windows примерно на 1 мегабайт, перенесла около 500 КB из read/write сегмента в read-only, а также уменьшила потребление оперативной памяти в общем примерно на 200 KB на каждый процесс Chrome. Удивительное заключается в том, что конкретно данная серия изменений состояла исключительно из удаления и добавления ключевого слова const в некоторых местах кода. Да, компиляторы — странные."
void constFunc(const int *x);
бессмысленно, как и константность возвращаемого значения, константность аргумента может иметь значение только в теле определения функции.C++, кстати, хоть и строже, но тоже может игнорировать константность возвращаемого значения и аргумента, но лень искать, где про это говориться явно.
Так что боюсь, все Ваши результаты про неизменность вызовов самоочевидны и без изучения ассемблера.
Это из Интел по-поводу возвращаемых значений:
Compiler generates this warning when it finds a type qualifier applied to the return type of a function. C++ allows the type-qualifier to be const or volatile, but either qualifier applied to a function return type is meaningless, because functions can only return rvalues and the type qualifiers apply only to lvalues.
Аналогичные предупреждения делают и многие другие компиляторы, хотя и не все.
Ну и выдержка из стандарта по-поводу аргументов:
Parameter declarations that differ only in the presence or absence of const and/or volatile are equivalent. That is, the const and volatile type-specifiers for each parameter type are ignored [...]
Only the const and volatile type-specifiers at the outermost level of the parameter type specification are ignored in this fashion; const and volatile type-specifiers buried within a parameter type specification are significant and can be used to distinguish overloaded function declarations. [...]
Для ссылок и указателей на константы это, естественно значимо. Тут я дал маху и поторопился.
Цитата из Интел тут ни к чему — Интел и их документация не славятся (мягко говоря) хорошим знанием языка С++. Вот и та цитата, которую вы здесь привели — безграмотная чушь. Эту чушь еще можно было худо-бедно подогнать под стандарт в рамках старинного С++98, но сегодня эта безграмотная чушь — безнадежна.
Цитата из стандарта языка, которую вы привели, описывает процесс формирования типа функции для целей overloading. Ни больше не меньше. Это совершено узкая, изолированная и посторонняя тема, не имеющая никакого отношения к рассматриваемому вопросу. К чему вы здесь ее привели?
В общем же ни о каком "ignored" для квалификаторов параметров и возвращаемого значения в С++ речи быть не может — они ни в коем случае не ignored (!).
Оба утверждения — не верны и являются популярной "пионэрской твердилкой".
Константность параметров, как вы сами сказали, влияет на семантику параметра в теле функции. В частности, такая константность активно и широко используется в coding standards, которые запрещают менять значения параметров внутри функции.
Константность возвращаемого значения действительно бессмысленна лишь для скалярных типов. Как только возвращаемое значение становится класс-типом эта константность тоже начинает влиять на семантику.
Про возвращаемое значение: скалярность и нескалярность не имеет значения, важно лишь возвращаем мы значение или ссылку/указатель. Возвращяемое значение — всегда rvalue, и константность/волатильность тут применена не может. Если мы возвращаем ссылку/указатель, то квалификатор того, на что они указывают, используется, т.к. мы можем обращаться к объекту через указатель, который сам по себе является rvalue.
В прототипе функции то же самое: если там значение, то квалификатор никак не используется, прототипы U f(T i); и U f(T const i); эквивалентны. А вот в определении параметры — это объявление локальных переменных и квалификация учитывается компилятором.
Так что нет, руководство компилятора Intel право, а Вы ошибаетесь: скалярный тип или класс — неважно, роль играет только возврат/передача параметра по значению или по ссылке. Что и требовалось доказать.
Да, именно словоблудие! Причем на словоблудие это я/мы им указывали не раз и по своей инициативе, и в процессе оплаченных Интелом (!) же ревью их документации, но воз, как говорится, и ныне там. Но основная проблема, боюсь, не в интеловской документации, а лично в вашем неумении понимать прочитанное.
Про возвращаемое значение — совершенно неверно! Тот факт, что возвращаемое значение является rvalue абсолютно ничего не меняет. Зачем вы это повторяете? Еще раз: в языке С++ для rvalue класс-типов cv-квалификация совершенно полноценно применима и всегда была применима (!), как я уже ясно сказал выше. Вы же почему-то продолжаете твердить, что она якобы "тут применена не может". Что за чушь? Попробуйте, в конце концов, сами
std::string foo() { return ""; }
const std::string bar() { return ""; }
int main()
{
foo() = "ABC";
bar() = "ABC";
}
Замечаете разницу? То-то. А далее уже самостоятельно: вперед изучать свойства cv-квалификации в С++.
Что касается параметров, вы вдруг полезли рассказывать про эквивалентность прототипов. Это так, но к чему вдруг здесь прототипы? Прототип описывает внешнюю спецификацию функции и она действительно не меняется от cv-квалификации параметров. Но к делу это вообще никак не относится. Еще раз, как уже было ясно сказано выше: cv-квалификация параметра имеет абсолютно явный и однозначный эффект по отношению к семантике этого параметра внутри определения функции. Прототипы к этому никакого отношения не имеют. (P.S. В С++ нет "прототипов").
По поводу параметров и отсутствия прототипов функций в C++ — не убедили. Возможно, в standardize последних версий термина «прототип функции» и нет, но используется он в отношении C++ повсеместно.
const в C/C++ нужен для увеличения числа случаев, когда возникает UB.
Я слышал, если производительность действительно важна, то нужно было рефакторить код, чтобы добавить побольше const, даже если код становился менее читабельным
А есть пример, где const как-то необратимо снижает читабельность?
Как и уже многие написали выше, я всегда считал, что const нужен больше как раз для читабельности и задания семантики. Писать на языке без констант — боль.
Вы ж не станете отрицать, что у кого-либо вообще может в голове возникнуть вопрос «а как, собственно, const может влиять на производительность?».
void constByArg(const int *x)
{
printf("%d\n", *x);
constFunc(x);
printf("%d\n", *x);
}
Это означает только, что функция constByArg не изменяет значение по указателю x. Однако никто не гарантирует, что это значение не может быть изменено вызовом функции constFunc.
int x = 42;
void constFunc(const int*)
{
x = 0;
}
int main()
{
constByArg(&x);
}
А выигрыш в работе непосредственно на компьютере — и в самом деле, при хорошем компиляторе не должен быть сильно заметен.
Почему const не ускоряет код на С/C++?а что, должен был ускорять?
Лично я думал, что препроцессор компилятора заменяет все упоминания константы на ее значение, то есть если
const int a = 4;
…
b = a * 2
то для компилятора это будет
…
b = 4 * 2 8
Вот я и говорю, я думал, что это делает препроцессор. Кстати, а что ему мешает делать такую замену? Причем я имею ввиду не аргумент, разумеется, а там где грубо говоря человек может заменить. (как в моем примере выше)
Мешает ему это делать тот простой факт, что язык препроцессора — это чуть ли не отдельный язык программирования, и const не является в нём ключевом словом.
Окей, понял. Тогда что мешает компилятору сделать то же самое? Я не сишник, помилуйте.
Обычная путаница, учитывая что константные переменные (константы) и константы препроцессора называются одним словом. Легко запутаться.
Путаница вызвана в первую очередь незнанием терминологии.
В языке С есть термин "константа". Константами в С называются буквальные значения (1
, 'a'
, 3.14
) и элементы enum. Далее все покрывается термином "константное выражение". const
-объекты в C "константами" не называются вообще и константных выражений не формируют.
В языке С++ термина "константа" применяется только к элементам enum. Буквальные значения называются "литералами". Все остальное выражается через термин "константное выражение". Характерно то, что в С++, в отличие от С, const
-объекты формируют константные выражения.
Какое-то очередное "открытие Америки".
Во-первых, const
, разумеется, "ускоряет" код в тех ситуациях, когда он применяется к самому объекту, а не к "пути доступа" к объекту. Это прекрасно видно во всем, включая сгенерированный компилятором код. В применении же к путям доступа const
действительно является лишь синтаксическим сахаром. Для оптимизации кода в таких ситуациях компилятору нужно знать полную картину aliasing, а const
в этом никак не помогает. Для этого служит restrict
.
Во-вторых, разглядывать для этих целей код, сгенерированный какими-то компиляторами — бессмысленно, особенно когда речь идет о GCC. GCC — отстал, крив и пионерск. Сегодня он не умеет что-то оптимизировать, но завтра — научится.
разглядывать для этих целей код, сгенерированный какими-то компиляторами
Я извиняюсь, а код сгенерированный чем вместо компилятора вы предлагаете вместо этого рассматривать?
Я предлагаю рассматривать чисто теоретические соображения о том, какие оптимизации возможны, а какие — нет в рамках стандартной семантики языков С и С++. Если оптимизация теоретически возможна в рамках данного языка, то ее практическая реализация является лишь вопросом времени. То, что какая-то оптимизация еще не реализована неким компилятором, ничего не значит.
Не забывайте, что оптимизации в современных компиляторах С/С++ реализуются не на основе некоего "суперумного самообучающегося искусственного интеллекта", а на основе банальных механизмов pattern matching. Что будет и что не будет оптимизировать компилятор зависит только от того, какой набор оптимизационных паттернов в него уже успел вбить некий условный Вася Пупкин (в промежутках между сдачами сессий), и какой набор паттернов он НЕ успел вбить (в том числе потому, что бумажка с описанием соответствующего паттерна завалилась за шкаф).
Разглядывая сгенерированный компиляторами код вы фактически разглядываете последствия Васиного рабочего графика, Васиного энтузиазма или Васиной лени. Не надо на основе этого делать выводы об эффективности тех или иных конструкций языка. Особенно если учесть что репутация GCC сегодня лежит ниже плинтуса во многом из-за огромного баклога на фиксинг багов и реализацию критически необходимых фич.
Ерундовая статья.
Сходите к Косте Осипову, если писать не о чем.
Причем, строго говоря, из флеша данные читаются медленнее, но ОЗУ мало, поэтому приходится жертвовать локальной оптимизацией во имя глобальной.
Рискну предположить, что const является семантическим правилом, работающим на этапе анализа исходного кода, и на генерацию целевого кода, скорее всего, ни как не влияющим.
Пометка значения как константа — это скорее семантическое правило "любой код, который попытается изменить это значение — неверен" чем оптимизация (хотя, возможно, какой — то из компиляторов и умеет использовать это при генерации целевого кода).
void constFunc(const int * const x);
А в чём смысл? Компилятор и так знает что переменная x не изменяется.
const int * x
это указатель, который низачто не изменит адрес, на который ссылается, но вот данные могут меняться:
int * const x
ну и как написали выше, это уже указатель, который гарантированно не изменит свой адрес и ссылается на НЕизменяемые данные:
const int * const x
Но если привести реальный пример тотальной оптимизации, то условия будут такими:
1 Функция имеет в параметрах внешние константы — состояние которых известно компилятору. Это может быть обычные перемененные, но иницилизированные константами в пределах видимости вызова функции.
2 Функция выполняется один раз, либо функция определена как static inline. Это позволит разместить новый экземпляр функции в месте применения — сделав её одноразовой.
3 Результат вычислений должен иметь повторяемое практическое значение в общем алгоритме. В случае разного применения результата функции — оптимизация может сбойнуть.
Только в этом случае функция на константах полностью оптимизируется до одной записи результата вычислений. То-есть полностью выполняется препроцессором компилятора.
Функции на константах предназначены для замены многоэтажных зубодробительных макросов. Алгоритм в виде функции проще воспринимается и намного легче редактируется.
Функции на константах применяются в основном для смены формата имеющихся известных данных — посчитать один раз, и больше не мучится.
void foo(int *p) { // Needs to do more copying of data } void foo(const int *p) { // Doesn't need defensive copies } int main() { const int x = 42; // const-ness affects which overload gets called foo(&x); return 0; }
С одной стороны, я не думаю, что на практике это часто применяется в С++-коде.
Просто резануло глаза. Применяется очень часто.
> Возможно, нам нужен компилятор поумнее. Скажем, Clang.
Не равнозначен исходному тексту:
> Maybe we just need a sufficiently smart compiler. Is Clang any better?
— В оригинале автор не утверждает, что «Clang поумнее», как это сделано в переводе. В оригинале автор интересуется: «окажется ли Clang чем-то лучше?»
Почему-то не нашел в статье именно внятного ответа на вопрос "Почему" в заголовке. Давайте поиграем в телепатов и найдём правдоподобные объяснения для данного кейса с SQLite.
С языком Си понятно — const
там просто обещание прогера не менять переменную после инициализацию, иначе UB будет. Что в С++:
Если в каком-нибудь примере заменить
const int a = 4
наint a = 4
, то переменная все равно останется "effectively const", потому что ее значение в коде не меняется, и "достаточно умный компилятор (тм)" должен по идее сам отловить, что переменная и есть константа (точнее, оба варианта должны работать одинаково), неважно какой код написан. Можно считатьconst
синтаксическим сахаром.
"По идее" const/read-only данные пихаются в другой сегмент данных, например,
.rdata
. Но в чем смысл это делать для одной автоматической переменной? Намного быстрее оставить переменную на стеке, как если бы это был не const. И там все остальные переменные рядом, не надо лазить туда-сюда по сегментам.
Ускорять код const будет преимущественно тогда, когда на его месте по хорошему должен быть constexpr. Собственно зачем constexpr и был введен в C++. Ведь по сути const — это не гарантия того, что объект не может меняться. Это лишь аттрибут типа, по сути дела — часть контракта по использованию конкретно типа. Если в функцию передан указатель на const объект — функция не может его изменить.
А вот constexpr, с другой стороны, способствует массовым compile time оптимизациям, когда обращения к константам заменяются на inline значения в коде
У вас наблюдается все та же путаница между двумя принципиально разными типами константности: константностью самого объекта и константностью пути доступа к объекту.
Константность объекта — это в С и С++ всегда гарантия того, что объект не может меняться (кроме mutable
членов объектов классов).
Константность — это лишь гарантия что через данный конкретный "путь", как вы выразились, нельзя обьект поменять. Пример:
void doSomething(const std::string& str)
{
}
...
std::string nonConstantString { "I am not a constant string" };
...
doSomething(nonConstantString);
в этом примере внутри "doSomething" строка видна только по "константному" пути, но сама "nonConstantString" не константна ни коем образом, более того значение в теории может поменяться даже пока вызов находится внутри "doSomething", так что никаких оптимизаций компилятор тут не будет иметь права сделать.
Прекрасно, но к чему это здесь?
Еще раз: вы сделали утверждение, что "const — это не гарантия того, что объект не может меняться". Я вас поправляю: const
верхнего уровня (т.е. примененный непосредственно к объекту, а не к пути доступа) является гарантией того, что объект не может меняться.
Ни больше, ни меньше.
Прекрасно, но к чему это здесь?
Точно такой же вопрос можно задать на ваш первый ответ мне.
Я вас поправляю: const верхнего уровня (т.е. примененный непосредственно к объекту, а не к пути доступа) является гарантией того, что объект не может меняться.
К сожалению это не совсем так, есть ведь const_cast. Разработчики стандарта давно говорят что это очень плохой механизм, но убрать его нельзя потому что половину кода придеться редезайнить
Нет, это не "не так". const_cast
в С++ предназначен для вполне конкретной цели — снятие константности с пути доступа к объекту, т.е. константности первого или более глубоких уровней вложенности. В С++ не существует средств снятия константности нулевого (верхнего) уровня, т.е. константности самого объекта и const_cast
вам ничем здесь не поможет.
В любом случае, попытки модификации константного объекта через неконстантный путь доступа (как бы вы его ни получили) НЕ модифицируют объект, а приводят в неопределенному поведению. Так что, еще раз: в С++ const
верхнего уровня является гарантией того, что объект не может меняться.
В любом случае, попытки модификации константного объекта через неконстантный путь доступа (как бы вы его ни получили) НЕ модифицируют объект, а приводят в неопределенному поведению.
Именно неопределенное поведение, т.е может как модифицировать объект, а может и access violation получить. Тут уж как повезет.
Например, такой код
struct A
{
int a;
A() { a = 5; }
};
const A a;
int main()
{
const_cast<A&>(a).a = 3;
std::cout << a.a;
return 0;
}
компилируется (и msvc и GCC компиляторами) и печатает 3 а не 5.
А вот такой:
const int c = 5;
int main()
{
const_cast<int&>(c) = 3;
std::cout << c << std::endl;
return 0;
}
просто крешится с access violation.
Это совершенно не важно, что получается в результате неопределенного поведения и как ведут себя разные компиляторы, потому что программа с неопределенным поведением не является корректной программой на языках С или С++. Анализировать поведение вашего кода и строить из него какие-то выводы — это по сути то же самое, что анализировать содержимое неинициализированной переменной. С точки зрения языка С++ в ваших экспериментах фактически нет никакой программы, а есть просто "мусор", который остался после компиляции (компилятор просто забыл или не сумел его почистить). И вы зачем-то пытаетесь не только запускать этот "мусор" (!), но еще и разглядывать результаты (!!). Зачем?
Ключевым моментом здесь является то, компиляторы С и С++ имеют право транслировать код исходя из предположения, что неопределенное поведение никогда не возникает (напр. компилятор С или С++ имеет право заменить целое d / d
на константу 1
даже если есть вероятность того, что d
окажется равным 0
). Именно в этом заключается суть утверждения "в С++ const
верхнего уровня является гарантией того, что объект не может меняться"
Почему const не ускоряет код на С/C++?