Pull to refresh

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&& не может быть универсальной ссылкой. Но я решил не обсуждать rvalue ссылки на константу, статья и так чрезмерно большая. Согласен, что можно придумать для них применение, но чем-то надо жертвовать.

Я этот момент не критикую. Статья действительно лбъёмная. 👍

Это на случай если кто задастся вопросом что с этим можно сделать.

Кстати, вроде как отказались от термина "универсальная ссылка" и плавильней "forwarding reference", наверное перевести стоит как "передающая ссылка".

В [VJG] forwarding reference переводится как передаваемая ссылка, и мне кажется по смыслу это больше подходит, английские отглагольные определения не всегда можно так просто перевести. А к универсальной ссылке все (включая меня) привыкли благодаря Скотту Мейерсу.

Все пошло криво, когда pointer перевели как ссылку (кстати, довольно двусмысленно, почему не ккакалка). А это просто адресс.

Проблема в том что термин "ссылка" применяется очень широко и, соответственно, имеет разный смысл в разных контекстах. Указатель - это тоже ссылка, если трактовать его в широком смысле. В C++ ссылка (reference) имеет вполне конкретное и достаточно узкое значение.

T &tx

Я T &tx не люблю. T& tx и делайте со мной что хотите!

И я тоже. И [VJG] тоже (с развернутой аргументацией). Здесь мы имеем дело с старыми сишными правилами, которые действительно многих раздражают. Я сам долго думал, как с этим быть, и решил все-таки остановится на каноническом синтаксисе, хотя в своих проектах я его не использую.

Процитирую:

А вот использовать ссылку в качестве параметра функции безопасно, в этом случае ссылка тоже копируется, но логика работы стека вызовов не позволяет появиться висячей ссылке (за исключением какой-нибудь явной патологии).

Сильно не согласился бы на счёт "безопасности". Классическая ошибка, и она у вас приведена же, когда в функцию по константной ссылке приходит временный объект и ссылка на него же и возвращается, и далее где-то присваивается и превращается в висячую. Ошибка как правило возникает, когда временный объект конструируется не явно, например, в результате преобразования типов, для программиста такая проблема может оказаться сразу не очевидной. Особенно если код возник не сразу, а пришёл к такому виду в результате постепенной эволюции.

Про 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 - я не знал, что его могут называть иначе.

Спасибо, рад, что мои материалы оказались полезными.

Sign up to leave a comment.

Articles