Почему const-правильность специфична для C++?
Отказ от ответственности: я знаю, что есть два вопроса о полезности const-правильности, однако ни один не обсуждал, как const-правильность необходима в C++ в отличие от других языков программирования. Кроме того, я не удовлетворен ответами на эти вопросы.
Сейчас я использовал несколько языков программирования, и одна вещь, которая меня беспокоит в C++, это понятие const-правильности. Нет такого понятия в Java, C#, Python, Ruby, Visual Basic и т. Д., Это, кажется, очень специфично для C++.
Прежде чем вы отправите меня в C++ FAQ Lite, я прочитал его, и это меня не убеждает. Совершенно достоверные, надежные программы написаны на Python все время, и здесь нет никакого ключевого слова const или его эквивалента. В Java и C# объекты могут быть объявлены как final (или const), но в них нет функций-членов const или параметров функции const. Если функция не нуждается в изменении объекта, она может использовать интерфейс, который предоставляет только доступ для чтения к объекту. Эта техника может в равной степени использоваться в C++. В двух реальных системах C++, над которыми я работал, const практически не использовался, и все работало нормально. Так что я далек от продажи полезности того, чтобы позволить const загрязнять базу кода.
Мне интересно, что в C++ делает const необходимым, в отличие от других языков программирования.
До сих пор я видел только один случай использования const:
#include <iostream>
struct Vector2 {
int X;
int Y;
};
void display(/* const */ Vector2& vect) {
std::cout << vect.X << " " << vect.Y << std::endl;
}
int main() {
display(Vector2());
}
Компиляция этого с закомментированным const принята Visual Studio, но с предупреждением C4239, используется нестандартное расширение. Таким образом, если вы хотите получить синтаксическую краткость передачи временных данных, избегая копий и соблюдая стандарты, вы должны пройти по константной ссылке, никак не обходя стороной. Тем не менее, это больше похоже на причуду, чем фундаментальная причина.
В противном случае, на самом деле не существует ситуации, когда необходимо использовать const, кроме случаев взаимодействия с другим кодом, который использует const. Const кажется мне ничем иным, как самодовольной чумой, которая распространяется на все, к чему она прикасается:
Причина, по которой const работает в C++, заключается в том, что вы можете от него отказаться. Если бы вы не могли выбросить это, тогда ваш мир был бы отстойным. Если вы объявите метод, который принимает const Bla, вы можете передать его неконстантному Bla. Но если все наоборот, ты не сможешь. Если вы объявляете метод, который принимает неконстантный Bla, вы не можете передать ему константный Bla. Так что теперь вы застряли. Таким образом, вам постепенно понадобится константная версия всего, что не константно, и вы получите теневой мир. В C++ вам это сходит с рук, потому что, как и в случае с C++, это совершенно необязательно, хотите вы эту проверку или нет. Вы можете просто убрать константу, если она вам не нравится.
Андерс Хейлсберг (архитектор C#), CLR Design Choices
14 ответов
Ну, мне понадобится 6 лет, чтобы по-настоящему понять, но теперь я, наконец, могу ответить на свой вопрос.
Причина, по которой C++ имеет "постоянную корректность", а не Java, C# и т. Д., Заключается в том, что C++ поддерживает только типы значений, а эти другие языки поддерживают только или, по крайней мере, по умолчанию ссылаются на типы.
Давайте посмотрим, как C#, язык по умолчанию ссылочных типов, имеет дело с неизменяемостью, когда задействованы типы значений. Допустим, у вас есть тип изменяемого значения, а другой тип имеет поле только для чтения:
struct Vector {
public int X { get; private set; }
public int Y { get; private set; }
public void Add(int x, int y) {
X += x;
Y += y;
}
}
class Foo {
readonly Vector _v;
public void Add(int x, int y) => _v.Add(x, y);
public override string ToString() => $"{_v.X} {_v.Y}";
}
void Main()
{
var f = new Foo();
f.Add(3, 4);
Console.WriteLine(f);
}
Что должен делать этот код?
- не скомпилировать
- печать "3, 4"
- вывести "0, 0"
Ответ № 3. C# пытается использовать ваше ключевое слово "только для чтения", вызывая метод Add для одноразовой копии объекта. Это странно, да, но какие еще есть варианты? Если он вызывает метод в исходном векторе, объект изменится, нарушив "только для чтения" поля. Если он не скомпилируется, тогда члены типа значения "только для чтения" довольно бесполезны, потому что вы не можете вызывать для них какие-либо методы, опасаясь, что они могут изменить объект.
Если бы только мы могли пометить, какие методы безопасны для вызова на экземплярах только для чтения... Подождите, это именно то, что методы const в C++!
C# не беспокоится о методах const, потому что мы не используем такие типы значений в C#; мы просто избегаем изменяемых типов значений (и объявляем их "злыми", см. 1, 2).
Кроме того, ссылочные типы не страдают от этой проблемы, потому что когда вы помечаете переменную ссылочного типа как только для чтения, то только для чтения является ссылка, а не сам объект. Это очень легко реализовать для компилятора, он может пометить любое присваивание как ошибку компиляции, кроме как при инициализации. Если все, что вы используете, это ссылочные типы и все ваши поля и переменные доступны только для чтения, вы получаете неизменность везде с небольшими синтаксическими затратами. F# работает полностью так Java избегает этой проблемы, просто не поддерживая определяемые пользователем типы значений.
C++ не имеет понятия "ссылочные типы", только "типы значений" (в C#-линго); некоторые из этих типов значений могут быть указателями или ссылками, но, как и типы значений в C#, ни один из них не владеет своим хранилищем. Если бы C++ обрабатывал "const" в своих типах так же, как C# обрабатывает "только для чтения" в типах значений, это было бы очень запутанным, как показано в примере выше, не обращая внимания на неприятное взаимодействие с конструкторами копирования.
Таким образом, C++ не создает одноразовую копию, потому что это создаст бесконечную боль. Это также не запрещает вам вызывать какие-либо методы для членов, потому что, ну, язык тогда не будет очень полезным. Но он все еще хочет иметь некоторое понятие "только для чтения" или "постоянство".
C++ пытается найти средний путь, заставляя вас пометить, какие методы безопасно вызывать для константных членов, а затем он доверяет вам, чтобы вы были точными и точными в вашей маркировке, и напрямую вызывает методы для исходных объектов. Это не идеально - это многословно, и вы можете нарушать постоянство столько, сколько пожелаете - но это, возможно, лучше, чем все другие варианты.
Корректность Const дает C++ два заметных преимущества, о которых я могу подумать, одно из которых делает его довольно уникальным.
- Это позволяет распространять понятия изменчивых / неизменных данных, не требуя связки интерфейсов. Отдельные методы могут быть аннотированы относительно того, могут ли они выполняться на объектах const, и компилятор обеспечивает это. Да, иногда это может быть хлопотно, но если вы используете его постоянно и не используете
const_cast
у вас есть проверенная компилятором безопасность в отношении изменяемых и неизменяемых данных. - Если объект или элемент данных
const
Компилятор может свободно размещать его в постоянной памяти. Это может особенно иметь значение во встроенных системах. C++ поддерживает это; немногие другие языки делают. Это также означает, что в общем случае вы не можете безопасно разыгратьconst
далеко, хотя на практике вы можете сделать это в большинстве сред.
C++ - не единственный язык с правильной константой или чем-то в этом роде. OCaml и Standard ML имеют похожую концепцию с различной терминологией - почти все данные являются неизменяемыми (const), и когда вы хотите, чтобы что-то было изменяемым, вы используете другой тип (a ref
типа), чтобы сделать это. Так что это просто уникально для C++ в соседних языках.
Наконец, пришло другое направление: были времена, когда я хотел использовать const в Java. final
иногда недостаточно далеко заходит создание простых неизменяемых данных (особенно неизменяемых представлений изменяемых данных) и не требуется создавать интерфейсы. Посмотрите на поддержку коллекции Unmodifiable в Java API и тот факт, что во время выполнения он только проверяет, разрешено ли изменение, для примера того, почему const полезен (или, по крайней мере, структура интерфейса должна быть углублена, чтобы иметь List и MutableList) - там не является причиной того, что попытка изменить неизменную структуру не может быть ошибкой типа компиляции.
Я не думаю, что кто-то утверждает, что правильность констант является "необходимой". Но опять же, классы тоже не нужны, не так ли? То же самое касается пространств имен, исключений,... вы получите картину.
Const-правильность помогает отлавливать ошибки во время компиляции, и поэтому это полезно.
const - это способ выразить что-то. Это будет полезно на любом языке, если вы считаете, что это важно выразить. У них нет этой функции, потому что разработчики языка не нашли их полезными. Думаю, если бы эта функция была там, она была бы такой же полезной.
Я отчасти думаю об этом как о спецификациях броска в Java. Если они вам нравятся, вы, вероятно, хотели бы их на других языках. Но дизайнеры других языков не думали, что это так важно.
Вы правы, const-правильность не нужна. Вы, безусловно, можете написать весь свой код без ключевого слова const и заставить все работать, как вы делаете это в Java и Python.
Но если вы сделаете это, вы больше не получите помощь компилятора в проверке нарушений const. Ошибки, о которых компилятор сообщал бы вам во время компиляции, теперь будут обнаруживаться только во время выполнения, если они вообще есть, и поэтому вам потребуется больше времени для диагностики и исправления.
Таким образом, попытка подорвать или избежать использования функции const -корректности просто усложняет ситуацию в долгосрочной перспективе.
Программирование - это запись на языке, который в конечном итоге будет обрабатываться компьютером, но это одновременно и способ общения с компьютером и другими программистами в одном проекте. Когда вы используете язык, вы ограничены концепциями, которые можно выразить в нем, и const - это еще одна концепция, которую вы можете использовать для описания своей проблемы и своего решения.
Постоянство позволяет вам четко выразить от разработки до кода одну концепцию, которой нет в других языках. Поскольку вы пришли из языка, в котором его нет, вам может показаться озадаченным концепция, которой вы никогда не пользовались - если вы никогда не использовали ее раньше, насколько это важно?
Язык и мысль тесно связаны. Вы можете выражать свои мысли только на том языке, на котором говорите, но язык также меняет способ вашего мышления. Тот факт, что у вас не было ключевого слова const в языках, с которыми вы работали, означает, что вы уже нашли другие решения для тех же проблем, и эти решения кажутся вам естественными.
В вопросе вы утверждали, что можете предоставить не мутирующий интерфейс, который может использоваться функциями, которым не нужно изменять содержимое объектов. Если вы подумаете об этом на секунду, это же предложение говорит вам, почему const - это концепция, с которой вы хотите работать. Необходимость определить не изменяющий интерфейс интерфейс и реализовать его в своем классе - это работа вокруг того факта, что вы не можете выразить эту концепцию на своем языке.
Постоянство позволяет вам выразить эти понятия на языке, понятном компилятору (и другим программистам). Вы устанавливаете компромисс в отношении того, что вы будете делать с параметрами, которые вы получаете, ссылками, которые вы храните, или определяете ограничения на то, что пользователям вашего класса разрешено делать с указанными вами ссылками. Практически каждый нетривиальный класс может иметь состояние, представленное атрибутами, и во многих случаях существуют инварианты, которые должны быть сохранены. Язык позволяет вам определять функции, которые предлагают доступ к некоторым внутренним данным, и в то же время ограничивают доступ к представлению только для чтения, которое гарантирует, что никакой внешний код не нарушит ваши инварианты.
Это концепция, по которой я скучаю больше при переходе на другие языки. Рассмотрим сценарий, в котором у вас есть класс C, который имеет среди прочего атрибут a типа A, который должен быть видимым для внешнего кода (пользователи вашего класса должны иметь возможность запрашивать некоторую информацию о a). Если у типа A есть какая-либо мутирующая операция, чтобы код пользователя не изменил ваше внутреннее состояние, вы должны создать копию a и вернуть ее. Программист класса должен знать, что должна быть выполнена копия, и должна выполнять (возможно, дорогостоящую) копию. С другой стороны, если бы вы могли выразить постоянство на языке, вы могли бы просто вернуть постоянную ссылку на объект (фактически ссылку на постоянное представление объекта) и просто вернуть внутренний элемент. Это позволит пользовательскому коду вызывать любой метод объекта, который отмечен как неизменяемый, тем самым сохраняя ваши инварианты класса.
Проблема / преимущество, все зависит от точки зрения, постоянства в том, что оно вирусное. Когда вы предлагаете постоянную ссылку на объект, могут быть вызваны только те методы, которые помечены как неизменяемые, и вы должны сообщить компилятору, какой из методов имеет это свойство. Когда вы объявляете константу метода, вы сообщаете компилятору, что пользовательский код, вызывающий этот метод, сохранит состояние объекта. Когда вы определяете (реализуете) метод с постоянной сигнатурой, компилятор будет напоминать вам о вашем обещании и фактически потребует, чтобы вы не изменяли данные внутренне.
Язык позволяет вам сообщать свойствам компилятора ваших методов, что вы не можете выразить их каким-либо другим способом, и в то же время компилятор сообщит вам, когда вы не соблюдаете свой дизайн, и попытается изменить данные.
В этом контексте никогда не следует использовать const_cast<>, так как результаты могут привести вас в область неопределенного поведения (как с языковой точки зрения: объект может находиться в постоянной памяти, так и с программной точки зрения).: вы можете нарушать инварианты в других классах). Но это, конечно, вы уже знаете, если вы читаете C++FAQ Lite.
Как примечание: последнее ключевое слово в Java не имеет ничего общего с ключевым словом const в C++, когда вы имеете дело со ссылками (в ссылках или указателях C++). Ключевое слово final изменяет локальную переменную, на которую оно ссылается, будь то базовый тип или ссылка, но не является модификатором упомянутого объекта. Таким образом, вы можете вызывать методы мутации через окончательную ссылку и, таким образом, предоставлять изменения в состоянии упомянутого объекта. В C++ ссылки всегда постоянны (вы можете привязать их только к объекту / переменной во время построения), а ключевое слово const изменяет, как пользовательский код может обрабатывать указанный объект. (В случае указателей вы можете использовать ключевое слово const как для элемента данных, так и для указателя: X const * const объявляет постоянный указатель на константу X)
Если вы пишете программы для встраиваемых устройств с данными во FLASH или ROM, вы не можете жить без правильной константности. Это дает вам возможность контролировать правильную обработку данных в разных типах памяти.
Вы также хотите использовать const в методах, чтобы воспользоваться преимуществами оптимизации возвращаемого значения. См. Скотт Мейерс Более эффективный C++ пункт 20.
Этот разговор и видео от Херба Саттера объясняют новые коннотации const
в отношении безопасности потоков.
Constness, возможно, не была тем, о чем вам раньше приходилось слишком беспокоиться, но с C++11, если вы хотите писать потокобезопасный код, вы должны понимать значение const
а также mutable
На самом деле, это не... не совсем так или иначе.
В других языках, особенно в функциональных или гибридных, таких как Haskell, D, Rust и Scala, у вас есть концепция изменчивости: переменные могут быть изменяемыми или неизменяемыми и обычно неизменяемыми по умолчанию.
Это позволяет вам (и вашему компилятору / интерпретатору) лучше рассуждать о функциях: если вы знаете, что функция принимает только неизменяемые аргументы, то вы знаете, что эта функция не изменяет вашу переменную и вызывает ошибку.
C и C++ делают нечто подобное, используя const, за исключением того, что это гораздо менее надежная гарантия: неизменность не обеспечивается; функция, расположенная ниже стека вызовов, может отбросить константу и изменить ваши данные, но это будет преднамеренным нарушением контракта API. Таким образом, намерение или лучшая практика - это работать так же, как неизменность в других языках.
Все это говорит о том, что в C++ 11 теперь есть реальное ключевое слово mutable, а также более ограниченное ключевое слово const.
В C, Java и C# вы можете узнать, посмотрев на сайт вызова, можно ли передать переданный объект функцией:
- в Java вы знаете, это определенно может быть.
- в C вы знаете, что это может быть только в том случае, если есть '&' или эквивалентный символ.
- в C# вы также должны сказать 'ref' на сайте вызовов.
В целом, в C++ вы не можете этого сказать, поскольку неконстантный ссылочный вызов выглядит идентично передаче по значению. Наличие константных ссылок позволяет вам устанавливать и применять соглашение C.
Это может существенно изменить читабельность любого кода, который вызывает функции. Что, вероятно, достаточно, чтобы оправдать языковую особенность.
Андерс Хейлсберг (C# architect): ... Если вы объявляете метод, который принимает неконстантный Bla, вы не можете передать ему константный Bla. Так что теперь вы застряли. Таким образом, вам постепенно понадобится константная версия всего, что не константно, и вы получите теневой мир.
Итак, еще раз: если вы начали использовать "const" для некоторых методов, вы обычно вынуждены использовать это в большей части своего кода. Но время, затрачиваемое на поддержание (типизацию, перекомпиляцию при отсутствии некоторого const и т. Д.) Константной корректности в коде, кажется больше, чем на исправление возможных (очень редких) проблем, вызванных полным отсутствием константной корректности. Таким образом, отсутствие поддержки const -корректности в современных языках (таких как Java, C#, Go и т. Д.) Может привести к немного сокращенному времени разработки для того же качества кода.
Запрос на улучшение для реализации правильности const существовал в процессе сообщества Java с 1999 года, но был закрыт в 2005 году из-за вышеупомянутого "загрязнения const", а также из-за причин совместимости: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4211070
Хотя язык C# не имеет конструкции const корректности, но аналогичные функциональные возможности, возможно, скоро появятся в "Контрактах кода Microsoft" (библиотека + инструменты статического анализа) для.NET Framework с использованием атрибутов [Pure] и [Immutable]: функции Pure в C#
Ключевое слово const в C++ (применительно к параметрам и объявлениям типов) - это попытка не дать программистам сбросить большой палец ноги и вытащить всю ногу в процессе.
Основная идея состоит в том, чтобы пометить что-то как "не может быть изменено". Тип const не может быть изменен (по умолчанию). Указатель const не может указывать на новое место в памяти. Просто, правда?
Ну, вот где приходит правильность const. Вот некоторые из возможных комбинаций, в которых вы можете оказаться, когда используете const:
Константная переменная Подразумевает, что данные, помеченные именем переменной, не могут быть изменены.
Указатель на переменную const Подразумевает, что указатель может быть изменен, но сами данные не могут.
Константный указатель на переменную Подразумевает, что указатель не может быть изменен (чтобы указать на новую область памяти), но что данные, на которые указывает указатель, могут быть изменены.
Константный указатель на константную переменную подразумевает, что ни указатель, ни данные, на которые он указывает, не могут быть изменены.
Вы видите, как некоторые вещи могут быть глупыми там? Вот почему, когда вы используете const, важно правильно указать, на какой const вы ставите метку.
Дело в том, что это просто хак во время компиляции. Маркировка просто говорит компилятору, как интерпретировать инструкции. Если вы выбросили из конста, вы можете делать все, что хотите. Но вам все равно придется вызывать методы, которые имеют постоянные требования, с типами, приведенными соответствующим образом.
Например, у вас есть функция:
void const_print(const char* str)
{
cout << str << endl;
}
Другой метод
void print(char* str)
{
cout << str << endl;
}
В основном:
int main(int argc, char **argv)
{
const_print("Hello");
print("Hello"); // syntax error
}
Это потому, что "hello" является указателем на константный символ, строка (в стиле C) помещается в постоянную память. Но в целом полезно, когда программист знает, что значение не будет изменено. Так что вместо ошибки сегментации можно получить ошибку компилятора. Как в ненужных заданиях:
const int a;
int b;
if(a=b) {;} //for mistake
Так как левый операнд является const int.