Разумно ли использовать в качестве аргумента std :: istream &&? - PullRequest
0 голосов
/ 16 января 2019

Я столкнулся с кодом, который делает это:

SomeObject parse (std::istream && input) {....

Аргумент input является rvalue-ссылкой, что обычно означает, что функция предназначена для того, чтобы стать владельцем аргумента. Это не совсем то, что здесь происходит.

Функция parse будет полностью потреблять входной поток, и она требует ссылки на rvalue, потому что тогда вызывающий код откажется от владения istream, и, следовательно, это сигнал о том, что входной поток будет недоступен.

Я думаю, что это нормально, потому что, поскольку функция parse на самом деле не перемещает объект, нет опасности вырезать подтип. По сути, это ведет себя как нормальная ссылка с точки зрения parse, только есть своего рода компилируемый комментарий к вызывающей функции, который вы должны оставить владельцу потока.

Действительно ли этот код безопасен? Или есть какая-то упущенная тонкость, которая делает это опасным?

Ответы [ 4 ]

0 голосов
/ 16 января 2019

Ваше предположение, что эталонный параметр rvalue подразумевает "вступление во владение", совершенно неверно. Ссылка Rvalue - это просто особая ссылка, которая поставляется с собственными правилами инициализации и правилами разрешения перегрузки. Не больше, не меньше. Формально он не имеет особого сходства с «перемещением» или «захватом» объекта, на который имеется ссылка.

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

Пример цитаты, похожей на ту, которую вы только что цитировали, фактически присутствует в самой стандартной библиотеке. Это дополнительные перегрузки, введенные в C ++ 11 (и C ++ 17, в зависимости от некоторых нюансов)

template< class CharT, class Traits, class T >
basic_ostream< CharT, Traits >& operator<<( basic_ostream<CharT,Traits>&& os, 
                                            const T& value );

template< class CharT, class Traits, class T >
basic_istream<CharT,Traits>& operator>>( basic_istream<CharT,Traits>&& st, T&& value );

Их основная цель - «преодолеть» разницу в поведении перегрузок между членами и не членами operator <<

#include <string>
#include <sstream>

int main() 
{
  std::string s;
  int a;

  std::istringstream("123 456") >> a >> s;
  std::istringstream("123 456") >> s >> a;
  // Despite the obvious similarity, the first line is well-formed in C++03
  // while the second isn't. Both lines are well-formed in C++11
}

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

0 голосов
/ 16 января 2019

std::move просто создает ссылку на значение из объекта и ничего более. Природа rvalue такова, что вы можете предположить, что никто не будет заботиться о его состоянии после того, как вы закончите с ним. std::move затем используется, чтобы позволить разработчикам давать обещания об объектах с другими категориями значений. Другими словами, вызов std::move в значимом контексте эквивалентен высказыванию «Я обещаю, что мне больше не важно состояние этого объекта».

Поскольку вы сделаете объект по существу непригодным для использования, и вы хотите убедиться, что вызывающий объект больше не будет использовать объект, использование ссылки на rvalue усиливает это ожидание в некоторой степени. Это заставляет звонящего дать это обещание вашей функции. Невыполнение обещания приведет к ошибке компилятора (при условии, что другой допустимой перегрузки нет). Не имеет значения, перемещаетесь ли вы с объекта или нет, только то, что первоначальный владелец согласился лишиться его владения.

0 голосов
/ 16 января 2019

То, что вы пытаетесь сделать здесь, не является «опасным» в том смысле, что, учитывая текущий интерфейс std::istream, кажется, что нет никаких обстоятельств, при которых ссылка на rvalue здесь обязательно приведет к неопределенности. поведение при получении ссылки на lvalue не будет иметь. Но семантика всей этой хитроумной штуки ИМХО в лучшем случае очень сомнительна. Что означает для вызывающего кода «отдавать право собственности», но в то же время «не передавать его»? Кому «принадлежит» поток после parse() возврата !? Каким образом parse() делает поток «непригодным для использования»? Что если синтаксический анализ завершится неудачно из-за какой-то ошибки до того, как весь поток будет «использован»? Поток "непригоден" тогда !? Никто не может попробовать прочесть остальное? Является ли "право собственности" как-то "возвращено" вызывающему коду в этом случае?

Поток - это абстрактное понятие. Цель потоковой абстракции - служить интерфейсом, через который кто-то может потреблять ввод, не зная, откуда поступают данные, где они живут, как к ним получают доступ и как ими управляют. Если цель parse() состоит в том, чтобы анализировать входные данные из произвольных источников, то это не должно касаться природы источника. Если это касается природы источника, то он должен запросить конкретный вид источника. И именно здесь, IMHO, ваш интерфейс противоречит сам себе. В настоящее время parse() принимает произвольный источник. Интерфейс говорит: я беру любой поток, который вы мне даете, мне все равно, как он реализован. Пока это поток, я могу работать с ним. В то же время, он требует от вызывающей стороны отказаться от объекта, который фактически реализует поток. Интерфейс требует, чтобы вызывающая сторона передала что-то, что сам интерфейс не позволяет какой-либо реализации за интерфейсом когда-либо знать, получать к ней доступ или использовать любым способом. Например, как бы я прочитал parse() из std::ifstream? Кто потом закрывает файл? Если не может быть парсером. Это также не может быть я, потому что вызов парсера заставил меня передать объект. В то же время я знаю, что синтаксический анализатор даже не мог знать, что должен закрыть файл, который я передал…

В конце концов, все будет работать правильно, потому что реализация интерфейса не могла бы сделать то, что интерфейс предлагал, и мой деструктор std::ifstream просто запустил бы и закрыл файл. , Но что именно мы получили, лгая друг другу так ?! Ты обещал захватить объект, когда ты никогда не собирался, я обещал никогда больше не трогать объект, когда знал, что мне всегда придется…

0 голосов
/ 16 января 2019

std::istream не может быть перемещен , поэтому практической пользы для этого нет.

Это уже говорит о том, что вещь может быть «модифицирована», не путаясь с предположением о том, что вы передаете право собственности на объект std::istream (что вы не делаете и не можете сделать).

Я могу вроде увидеть обоснование использования этого, чтобы сказать, что поток логически перемещается, но я думаю, что вы должны провести различие между "владением этой вещью передается", и «Я сохраняю право собственности на эту вещь, но я позволю вам использовать все ее услуги». Передача прав собственности довольно хорошо понимается как соглашение в C ++, и это не совсем так. Что подумают пользователи вашего кода, когда им придется написать parse(std::move(std::cin))?

Хотя ваш путь не "опасен"; вы не сможете ничего сделать с той ссылкой на rvalue, которую вы не можете сделать с ссылкой на lvalue.

Было бы гораздо более самодокументированным и обычным делом просто взять ссылку на lvalue.

...