Почему `void *` неявно приводится в C ++?

30

В C нет необходимости приводить void *к любому другому типу указателя, это всегда безопасно продвигается. Однако в C ++ это не так. Например,

int *a = malloc(sizeof(int));

работает в C, но не в C ++. (Примечание: я знаю, что вам не следует использовать mallocв C ++ или в этом отношении new, и вместо этого следует отдавать предпочтение умным указателям и / или STL; об этом спрашивается просто из любопытства) Почему стандарт C ++ не допускает такое неявное приведение, в то время как стандарт C делает?

wolfPack88
источник
3
long *a = malloc(sizeof(int));К сожалению, кто-то забыл изменить только один тип!
Довал
4
@Doval: это все еще легко исправить, используя sizeof(*a)вместо этого.
wolfPack88
3
Я считаю, что точка зрения @ ratchetfreak заключается в том, что причина того, что C делает это неявное преобразование, заключается в том, что он mallocне может вернуть указатель на выделенный тип. newis C ++ действительно возвращает указатель на выделенный тип, так что правильно написанному коду C ++ никогда не придется приводить void *s.
Gort the Robot
3
Кроме того, это не любой другой тип указателя. Применяются только указатели данных.
Дедупликатор
3
@ wolfPack88 у тебя неправильная история. C ++ был void, C сделал не . Когда это ключевое слово / идея было добавлено в C, они изменили его в соответствии с потребностями C. Это было вскоре после того, как типы указателей начали проверяться вообще . Посмотрите, можете ли вы найти в Интернете брошюру с описанием K & R C или винтажную копию программного текста на C, такого как C Primer от Waite Group . ANSI C был полон функций, перенесенных или вдохновленных C ++, а K & R C был намного проще. Поэтому более правильно, что C ++ расширил C, как он существовал в то время, а C, который вы знаете, был удален из C ++.
JDługosz

Ответы:

39

Потому что неявные преобразования типов, как правило, небезопасны, и C ++ занимает более безопасную позицию при наборе текста, чем C.

C обычно допускает неявные преобразования, даже если большинство шансов, что преобразование является ошибкой. Это потому, что C предполагает, что программист точно знает, что они делают, и если нет, то это проблема программиста, а не проблема компилятора.

C ++ обычно запрещает вещи, которые потенциально могут быть ошибками, и требует, чтобы вы явно указывали свое намерение с помощью приведения типа. Это потому, что C ++ пытается быть дружественным к программисту.

Вы можете спросить: почему это так дружелюбно, когда на самом деле вам нужно печатать больше?

Итак, вы видите, что любая строка кода в любой программе, на любом языке программирования, как правило, будет прочитана намного больше, чем будет написана (*). Таким образом, легкость чтения намного важнее, чем простота написания. А при чтении выделение любых потенциально небезопасных преобразований с помощью явного приведения типов помогает понять, что происходит, и иметь определенный уровень уверенности в том, что происходящее на самом деле и должно было произойти.

Кроме того, неудобство необходимости вводить явное приведение является тривиальным по сравнению с неудобствами, которые тратятся часами на устранение неполадок, чтобы найти ошибку, которая была вызвана ошибочным назначением, о котором вы могли быть предупреждены, но никогда не были.

(*) В идеале он будет записан только один раз, но он будет читаться каждый раз, когда кому-то нужно просмотреть его, чтобы определить его пригодность для повторного использования, и каждый раз, когда происходит устранение неполадок, и каждый раз, когда кому-то нужно добавить код рядом с ним, а затем каждый раз, когда происходит поиск неисправностей в соседнем коде, и так далее. Это верно во всех случаях, за исключением сценариев «однократная запись, запуск, затем выбрасывание», и поэтому неудивительно, что большинство языков сценариев имеют синтаксис, который облегчает написание с полным пренебрежением к простоте чтения. Вы когда-нибудь думали, что Perl совершенно непостижимо? Вы не одиноки. Думайте о таких языках как о языках только для записи.

Майк Накис
источник
С по сути на один шаг выше машинного кода. Я могу простить С за такие вещи.
Qix
8
Программы (независимо от языка) предназначены для чтения. Явные операции выделяются.
Матье М.
10
Стоит отметить, что забросы через void*являются более небезопасными в C ++, потому что с тем , как некоторые особенности ООП реализованы в C ++, указатель на тот же объект может иметь различное значение в зависимости от типа указателя.
Гайд
@MatthieuM. очень верно Спасибо за добавление, это стоит быть частью ответа.
Майк Накис
1
@MatthieuM .: Ах, но вы действительно не хотите делать все явно. Читаемость не улучшается благодаря большему чтению. Хотя в этом пункте баланс явно для того, чтобы быть явным.
Дедупликатор
28

Вот что говорит Страуструп :

В C вы можете неявно преобразовать пустоту * в T *. Это небезопасно

Затем он показывает пример того, как void * может быть опасен, и говорит:

... Следовательно, в C ++, чтобы получить T * из void *, вам нужно явное приведение. ...

Наконец он отмечает:

Одним из наиболее распространенных применений этого небезопасного преобразования в C является присвоение результата malloc () подходящему указателю. Например:

int * p = malloc (sizeof (int));

В C ++ используйте типобезопасный оператор new:

int * p = new int;

Об этом он подробнее расскажет в статье «Дизайн и эволюция C ++» .

Таким образом, ответ сводится к следующему: разработчик языка считает, что это небезопасный шаблон, и поэтому сделал его незаконным и предоставил альтернативные способы выполнения того, для чего этот шаблон обычно использовался.

Горт Робот
источник
1
Что касается mallocпримера, то стоит отметить, что malloc(очевидно) не будет вызывать конструктор, поэтому, если это тип класса, для которого вы выделяете память, возможность неявного приведения к типу класса может ввести в заблуждение.
chbaker0
Это очень хороший ответ, возможно, лучше, чем мой. Мне только немного не нравится подход «аргумент от власти».
Майк Накис
"new int" - вау, как новичок в C ++, я не мог придумать, как добавить базовый тип в кучу, и я даже не знал, что вы можете это сделать. -1 использовать для malloc.
Katana314
3
@MikeNakisFWIW, я хотел дополнить твой ответ. В этом случае вопрос был «почему они это сделали», поэтому я подумал, что услышать мнение главного дизайнера оправдано.
Gort the Robot
11

В C нет необходимости приводить void * к любому другому типу указателя, он всегда безопасно продвигается.

Это всегда рекламируется, да, но вряд ли безопасно .

C ++ отключает это поведение именно потому , что он пытается иметь более безопасную систему типов , чем C, и такое поведение не безопасно.


Рассмотрим в целом эти 3 подхода к преобразованию типов:

  1. заставить пользователя выписать все, чтобы все преобразования были явными
  2. предположим, что пользователь знает, что он делает, и конвертирует любой тип в любой другой
  3. реализовать полную систему типов с типобезопасными обобщениями (шаблоны, новые выражения), явные пользовательские операторы преобразования и принудительное явное преобразование только тех вещей, которые компилятор не может видеть, неявно безопасны

Что ж, 1 уродлив и является практическим препятствием для достижения чего-либо, но он может быть действительно использован там, где требуется большая осторожность. C примерно выбрал 2, который легче реализовать, и C ++ для 3, который сложнее реализовать, но безопаснее.

Бесполезный
источник
Это был долгий и трудный путь, чтобы добраться до 3, с исключениями, добавленными позже и шаблонами позже.
JDługosz
Что ж, шаблоны для такой полной безопасной системы типов на самом деле не нужны: языки на основе Хиндли-Милнера прекрасно обходятся без них, без неявных преобразований и вообще не требуют явной записи типов. (Конечно, эти языки полагаются на стирание типов / сбор мусора / полиморфизм с более высоким родом, то, чего С ++ предпочитает избегать.)
leftaroundabout около
1

По определению, пустой указатель может указывать на что угодно. Любой указатель может быть преобразован в пустой указатель, и, таким образом, вы сможете преобразовать обратно, получая точно такое же значение. Однако указатели на другие типы могут иметь ограничения, такие как ограничения выравнивания. Например, представьте архитектуру, в которой символы могут занимать любой адрес памяти, но целые числа должны начинаться с четных границ адреса. В некоторых архитектурах целочисленные указатели могут даже подсчитывать 16, 32 или 64 бита за раз, так что char * может фактически иметь кратное число числового значения int *, указывая на то же место в памяти. В этом случае преобразование из пустоты * фактически закруглит биты, которые не могут быть восстановлены и поэтому необратимы.

Проще говоря, указатель void может указывать на что угодно, включая вещи, на которые другие указатели могут быть не способны указывать. Таким образом, преобразование в указатель void безопасно, но не наоборот.

roserez
источник