Comments 26
Только мне кажется, что называть C++ ссылки во всех их ипостасях "ссылочными типами" -- по меньшей мере, преувеличение?
Ссылочный тип -- это тип доступный исключительно по ссылке, а не ограничение на использование обычного типа. Что-то похожее на ссылочные типы в плюсах -- это а)известные со времен C указатели на непрозрачные стуркутры и б)непрозрачные структуры/классы, завернутые в умные указатели и в)классы с PIMPL. Стрелочка вместо точки -- синтаксическая мелочь, которая ничего не меняет. В случае в) стрелочка и неособо нужна...
Не соглашусь с Вами. Ссылочный тип - это отдельный тип, но вторичный или производный, то есть образованный от другого типа (как и тип массива). Это хорошо видно, если использовать ссылочный тип в качестве аргумента шаблона, например std::pair<int&, int&>
. Ссылочный тип в некотором смысле ущербный, об этом я много писал. А тип переменной, на которую ссылка ссылается - это совсем другое. Здесь мы имеем проблему скорее относящуюся к русскому языку в котором подобные вещи не очень удобно различать. Это примерно тоже, что и разница между константным указателем и указателем на константу.
Не соглашусь с Вами
Сколько угодно, терминологические битвы неинтересны. Ссылочный тип -- устоявшийся термин, появившийся, если не ошибаюсь, в языках со сборкой мусора (вроде Java) для типов, экземпляры которых создаются исключительно в куче и доступных только через handle-ссылку.
В C++ ссылки -- это ограниченный в использовании указатель, не более, но и не менее. Называть какие-либо ссылки "ссылочными типами" -- вводить неискушенного читателя в заблуждение. Студпэр с двумя ссылками внутри -- это студпэр с двумя полями-указателями, просто язык затрудняет передачу в них nullptr. Но не запрещает. Как-то так, на скорую руку, не судите строго, могу напортачить, но идея должна быть ясна:
//...
std::pair<int&, int&> p;
{
int a = 15;
int b = 4;
p = std::pair<int&, int&>{a, b};
// а тут копируются инты? Думаю, адреса интов...
...
}
// что?!!!
Кстати, я, лично, полагаю, что в C++ нужны ссылочные типы: синтаксический сахар для создания "запимпленных" классов, экземпляр такого класса и будет служить handl'ом для неявного и доступного только в одной единице компиляции "внутреннего" типа.
Согласен, что в C#, Jave термин "ссылочный тип" имеет совсем другой смысл. Но в C++ шаблонах нужно как то называть тип самой ссылки, а не тип переменой, на которую она ссылается, без этого описание шаблонов невозможно. Посмотрите Вандевурд, Дэвид, Джосаттис, Николаи М., Грегор, Дуглас. Шаблоны C++. Справочник разработчика, 2-е изд. В этом справочнике как раз для этого и используется термин "ссылочный тип", так что я ничего не придумывал. Вот цитата
std::is_reference<T>::value
Дает true
, если T
представляет собой ссылочный тип.
Пустой какой-то спор, оба правы. Строго говоря, и ссылки и указатели в C++ называют ссылочными типами, если верить Wikipedia. :)
На мой вкус, это не совсем верно и, когда-то давно, когда учился в 90-х, их вроде по-русски даже называли косвенными типами. Хотя и это неверно.
Подытоживая: думайте, что хотите, но и в C, и в C++ адрес памяти -- совершенно законный целочисленный тип данных. У указателя может быть свой адрес, не связанный с адресуемым объектом. Не все то, что можно вытворить с указателем, можно делать с ссылкой, хотя они могут быть равны побитово. А ссылка -- указатель с дополнительными ограничениями и сахаром. Можно называть ссылки ссылочными типами, можно иначе, но это не добавляет ничего нового и не несет никакой пользы. Поэтому остаюсь при своем мнении.
Крутая статья, спасибо!
Мне как интересующемуся дизайном языков программирования в целом, интересно мнение практикующих С++ (и не только) программистов: насколько на ваш взгляд удобен и совершенен дизайн ссылок? Если бы вы разрабатывали язык с нуля без оглядки на обратную совмесимость, что бы вы изменили в ссылках?
Для затравки, я бы выделил как минимум следующие аспекты ссылок в разных языках:
явный (с разыменованием) или неявный (без разыменования) доступ к тому, на что указывает ссылка; в первом случае ссылки превращаются в "ненуллабельные указатели". Явный доступ более многословен, но и более очевиден.
наличие или отсутствие возможности изменения самой ссылки (перенаправления на другой объект в памяти); как вариант - наличие специального синтаксиса перенаправления
при присваивании ссылок друг другу, изменяются сами ссылки (C#) или значения, на которые они ссылаются (C++)
при передаче в функцию: явное указание передачи именно по ссылке (ref - C#, D) или отсутствие такого указания (C++)
наличие, кроме обычных ссылок, также RValue-ссылок, обладающих специальной семантикой (С++)
привязка способа передачи к типу. Объекты всех типов могут передаваться как по ссылке, так и по значению (С++), или некоторые типы всегда "ссылочные" а некоторые всегда "значения" (Java, C#, D), например принятие по умолчанию, что все классы "ссылочные" а все структуры "значения".
Спасибо! Вопросы интересные, но не простые, с ходу ответить не могу. Подробнее напишу немного позднее.
Одна из идей, которой руководствуются при проектировании ссылок – это сделать ссылку максимально неотличимой от объекта, на который она ссылается. Этим можно объяснить, почему при инициализации C++ ссылки не используется специальный оператор (типа &
в случае указателей) и не используется оператор разыменования (типа *
в случае указателей) для доступа к объекту. В результате возникают некоторые коллизии, но небольшие, например, нельзя перегрузить функции, у которых параметр передается по значению и по ссылке на константу.
Теперь про неизменяемость C++ ссылок, то есть отсутствие нулевых ссылок (обязательная инициализация) и невозможность перенаправить на другую переменную. Посмотрим, какие изменения в языке нужны, если бы ссылки были изменяемые, то есть допускали нулевое значение и перенаправление на другую переменную. Первая проблема – это как быть с присваиванием. Нужно отличать присваивание самих ссылок и присваивание объектов, на которые она ссылается. Для изменяемых ссылок логично реализовывать присваивание как присваивание самих ссылок (а как иначе организовать перенаправление?), но присваивание объектов тоже очень важно в C++. Вторая проблема – это использование самих ссылок в качестве выходного параметра (это еще один вариант реализовать перенаправление). Для этого надо было бы разрешить указатели на ссылку и ссылку на ссылку. Решение всех этих проблем усложнило бы и без того непростой C++, то есть неизменяемость C++ ссылок позволило не переусложнить язык.
Теперь посмотрим, что происходит в языках со сборкой мусора, например C#. Объекты, управляемые сборщиком мусора, доступны только через ссылку, таким образом, никаких коллизий между объектом и ссылкой на него быть не может, не нужен оператор разыменования. Ссылки на объекты, управляемые сборщиком мусора, являются изменяемыми, то есть могут быть нулевыми и при присваивании происходит присваивание самих ссылок. Но при этом сами объекты не поддерживают присваивание, для них нельзя перегрузить оператор =
, поэтому коллизий не возникает. Если для объекта требуется операция, аналогичная копированию или присваиванию, то в C# надо реализовать специальные методы, типа IClonable.Clone()
. Но потребность в таких операциях возникает крайне редко. Ссылки на объекты, управляемые сборщиком мусора, сами могут быть выходными параметрами функции, для таких параметров при объявлении и вызове надо использовать ключевое слово ref
.
Поэтому и нет std::optional<T&>.
Не смогли решить относиться к этому как к обёртке или сделать неотличимым от объекта.
Думаю, все-же правильнее относиться к ссылке как к обертке (неявному указателю), а не как ко второму имени объекта. То что компилятор в процессе оптимизации может вообще удалить объекты ссылок - ну так он много чего может, он может целые функции выкидывать...
Для неотличимости от объекта нужна другая языковая конструкция - "alias", в С/С++ ближе всего #define.

Кстати, о применимости const T&&. Если мы требуем именно временный объект.
Да, const T&&
не может быть универсальной ссылкой. Но я решил не обсуждать rvalue ссылки на константу, статья и так чрезмерно большая. Согласен, что можно придумать для них применение, но чем-то надо жертвовать.
Я этот момент не критикую. Статья действительно лбъёмная. ?
Это на случай если кто задастся вопросом что с этим можно сделать.
Кстати, вроде как отказались от термина "универсальная ссылка" и плавильней "forwarding reference", наверное перевести стоит как "передающая ссылка".
В [VJG] forwarding reference переводится как передаваемая ссылка, и мне кажется по смыслу это больше подходит, английские отглагольные определения не всегда можно так просто перевести. А к универсальной ссылке все (включая меня) привыкли благодаря Скотту Мейерсу.
Как оказывается Скотт даже предлагал forwarding reference в статье, но в итоге взял universal reference и усложнил всем жизнь :)
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4164.pdf
Все пошло криво, когда pointer перевели как ссылку (кстати, довольно двусмысленно, почему не ккакалка). А это просто адресс.
T &tx
Я T &tx не люблю. T& tx и делайте со мной что хотите!
Процитирую:
А вот использовать ссылку в качестве параметра функции безопасно, в этом случае ссылка тоже копируется, но логика работы стека вызовов не позволяет появиться висячей ссылке (за исключением какой-нибудь явной патологии).
Сильно не согласился бы на счёт "безопасности". Классическая ошибка, и она у вас приведена же, когда в функцию по константной ссылке приходит временный объект и ссылка на него же и возвращается, и далее где-то присваивается и превращается в висячую. Ошибка как правило возникает, когда временный объект конструируется не явно, например, в результате преобразования типов, для программиста такая проблема может оказаться сразу не очевидной. Особенно если код возник не сразу, а пришёл к такому виду в результате постепенной эволюции.
Про std::min и std::max
неплохо бы сказать, у вас как-то вскользь, что попытка им подсунуть временный объект и сохранить ссылку тоже -- баг:
const auto& min = std::min(f1(arg), f2(arg2))
-- как пример.
И про "ссылку" получаемую из итератора std::vector<bool>
тоже следовало бы упомянуть, попытка присвоить её обычной ссылке наталкивается на срок жизни временного объекта.
И про то, что offsetof()
для ссылок строго формально -- не применим, не сказано.
Не сказано так же о том, что взятие ссылки на временный объект продлевает его срок жизни до конца жизни самой ссылки. Толку от этого не много, такая ссылка работает не лучше и не хуже, чем переменная нужного типа. Но смысл появляется в случае, когда ссылка имеет тип базового класса, а тип самого временного объекта -- производный и явно не задан, не виден. Тогда ссылка хоть и представляется базовым классом, но при выходе из области видимости компилятор вызовет деструктор производного типа. Такой трюк использовался в C++03 обычно совместно с шаблоном (параметр которого и менял тип самого временного объекта). В современности ключевое слово auto делает данный трюк бессмысленным.
Спасибо за конструктивную критику.
А вот использовать ссылку в качестве параметра функции безопасно
Здесь я имел в виду передача ссылки в глубину стека, конечно возврат этой же ссылки может все сломать и я про это писал.
И про "ссылку" получаемую из итератора std::vector<bool> ...
Согласен, что итераторы, возвращающие прокси-объекты, не обсуждал. Эта тема подробно обсуждается у Скотта Мейерса.
Не сказано так же о том, что взятие ссылки на временный объект продлевает его срок жизни до конца жизни самой ссылки.
Не согласен, этому посвящен раздел 4.1.
3.4.1:
вывод типа
auto
и вывод типа аргумента шаблона это практически одно и то же
А разве в констексте лямбд auto и вывод типа аргумента шаблона это не <em>буквально</em> одно и то же? Ведь это значит, что аргумент, для которого написано auto, становится шаблонным параметром operator () для соответствующей создаваемой локальной структуры?
Вообще надо различать вывод аргумента шаблона и вывод типа параметра функции. Они могут не совпадать, типа параметра функции может быть украшен спецификатором ссылки, const
и другими модификаторами. В шаблонах функций аргумента шаблона нам доступен, но в лямбдах с auto
он будет скрытым.
Кстати, с C++20 такой синтаксис шаблонов можно использовать и для обычных функций
auto f(auto x)
{
...
}
Я опоздал на год, нашел статью только что. Огромное спасибо за такой разбор материала. И отдельное спасибо за приведение альтернативных обозначений для некоторых вещей, например как с forward declaration - я не знал, что его могут называть иначе.
Ссылки и ссылочные типы в C++