Следующий код выглядит довольно безобидным на первый взгляд. Пользователь использует функцию bar()
для взаимодействия с некоторыми функциями библиотеки. (Это могло работать даже долгое время, так как bar()
возвращало ссылку на временное значение или подобное.) Однако теперь оно просто возвращает новый экземпляр B
. B
снова имеет функцию, a()
которая возвращает ссылку на объект повторяемого типа A
. Пользователь хочет запросить этот объект, что приводит к segfault, поскольку B
возвращаемый временный объект bar()
уничтожается до начала итерации.
Я не знаю, кто (библиотека или пользователь) виноват в этом. Все предоставленные библиотекой классы кажутся мне понятными и, конечно, не делают ничего другого (возвращая ссылки на члены, возвращая экземпляры стека, ...), чем многие другие программы. Пользователь, похоже, тоже не делает ничего плохого, он просто перебирает какой-то объект, не делая ничего относительно времени жизни этого объекта.
(С этим может быть связан вопрос: следует ли установить общее правило, согласно которому код не должен «основываться на диапазоне для итерации» над чем-то, что извлекается более чем одним соединением вызовов в заголовке цикла, поскольку любой из этих вызовов может вернуть Rvalue?)
#include <algorithm>
#include <iostream>
// "Library code"
struct A
{
A():
v{0,1,2}
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
int * begin()
{
return &v[0];
}
int * end()
{
return &v[3];
}
int v[3];
};
struct B
{
A m_a;
A & a()
{
return m_a;
}
};
B bar()
{
return B();
}
// User code
int main()
{
for( auto i : bar().a() )
{
std::cout << i << std::endl;
}
}
Ответы:
Я думаю, что фундаментальная проблема - это сочетание языковых возможностей (или их отсутствие) в C ++. И библиотечный код, и клиентский код являются разумными (о чем свидетельствует тот факт, что проблема далеко не очевидна). Если бы время жизни временного
B
расширения было подходящим (до конца цикла), проблем не было бы.Заставить временную жизнь достаточно долго и больше не делать это чрезвычайно сложно. Даже довольно временное «все временные эффекты, вовлеченные в создание диапазона для диапазона, основанного на действии до конца цикла», не будут иметь побочных эффектов. Рассмотрим случай
B::a()
возврата диапазона, независимого отB
объекта, по значению. Тогда временныйB
может быть немедленно отброшен. Даже если бы можно было точно определить случаи, когда необходимо продление срока службы, поскольку эти случаи не очевидны для программистов, эффект (деструкторы называются гораздо позже) был бы удивительным и, возможно, не менее тонким источником ошибок.Было бы более желательно просто обнаружить и запретить такую ерунду, заставляя программиста явно повышать уровень
bar()
до локальной переменной. Это невозможно в C ++ 11 и, вероятно, никогда не будет возможно, потому что это требует аннотаций. Руст делает это, где подпись.a()
будет:Вот
'x
переменная или область времени жизни, которая является символическим именем для периода времени, в течение которого ресурс доступен. Честно говоря, времена жизни трудно объяснить - или мы еще не нашли лучшего объяснения - поэтому я ограничусь минимальным необходимым для этого примера и отсылаю склонного читателя к официальной документации .Средство проверки заимствования заметит, что результат
bar().a()
должен жить до тех пор, пока работает цикл. Выражаясь как ограничение на время жизни'x
, мы пишем:'loop <= 'x
. Было бы также заметить, что получатель вызова методаbar()
, является временным. Два указателя связаны с одним и тем же временем жизни, поэтому'x <= 'temp
существует другое ограничение.Эти два ограничения противоречивы! Нам нужно,
'loop <= 'x <= 'temp
но'temp <= 'loop
, который отражает проблему довольно точно. Из-за противоречивых требований ошибочный код отклоняется. Обратите внимание, что это проверка во время компиляции, и код Rust обычно приводит к тому же машинному коду, что и эквивалентный код C ++, поэтому вам не нужно платить за него время выполнения.Тем не менее, это большая возможность, которую можно добавить в язык, и она работает, только если ее использует весь код. Это также влияет на дизайн API (некоторые проекты, которые были бы слишком опасны в C ++, становятся практичными, другие нельзя сделать так, чтобы они хорошо играли с жизнями). Увы, это означает, что нецелесообразно добавлять в C ++ (или на любой другой язык) задним числом. Таким образом, ошибка заключается в инерции успешных языков и в том, что у Бьярне в 1983 году не было хрустального шара и предвидения, чтобы включить уроки последних 30 лет исследований и опыта C ++ ;-)
Конечно, это не поможет избежать проблемы в будущем (если вы не переключитесь на Rust и никогда не будете использовать C ++ снова). Можно было бы избежать более длинных выражений с несколькими цепочечными вызовами методов (что довольно ограниченно и даже удаленно не устраняет все жизненные проблемы). Или можно попытаться принять более дисциплинированную политику владения без помощи компилятора: четко документировать, что
bar
возвращается по значению и что результатB::a()
не должен переживать то,B
на чтоa()
вызывается. При изменении функции для возврата по значению вместо долговременной ссылки помните, что это изменение контракта . Все еще подвержен ошибкам, но может ускорить процесс выявления причины, когда это произойдет.источник
Можем ли мы решить эту проблему, используя функции C ++?
В C ++ 11 добавлены квалификаторы ref для функции-члена, что позволяет ограничить категорию значений экземпляра класса (выражения), к которому может быть вызвана функция-член. Например:
При вызове
begin
функции-члена мы знаем, что, скорее всего, нам также потребуется вызватьend
функцию-член (или что-то вродеsize
, чтобы получить размер диапазона). Это требует, чтобы мы оперировали lvalue, поскольку нам нужно обращаться к нему дважды. Поэтому вы можете утверждать, что эти функции-члены должны быть lvalue-ref-qualified.Тем не менее, это может не решить основную проблему: алиасинг. Функция- член
begin
иend
создает псевдоним объекта или ресурсов, которыми управляет объект. Если мы заменимbegin
иend
одной функциейrange
, мы должны обеспечить один , который можно назвать на rvalues:Это может быть допустимым вариантом использования, но приведенное выше определение
range
запрещает его. Так как мы не можем обратиться к временному объекту после вызова функции-члена, может быть более разумным вернуть контейнер, то есть собственный диапазон:Применение этого к делу ОП и небольшая проверка кода
Эта функция-член изменяет категорию значений выражения:
B()
является предварительным значением, ноB().a()
является левым значением. С другой стороны,B().m_a
это ценность. Итак, давайте начнем с того, чтобы сделать это последовательным. Есть два способа сделать это:Вторая версия, как сказано выше, исправит проблему в OP.
Кроме того, мы можем ограничить
B
функции-члены:Это не окажет никакого влияния на код OP, так как результат выражения после
:
цикла for в диапазоне зависит от ссылочной переменной. И эта переменная (как выражение, используемое для доступа к ее функциямbegin
иend
функциям-членам) является lvalue.Конечно, вопрос заключается в том, должно ли правило по умолчанию «наложение псевдонимов функций-членов на rvalues должно возвращать объект, который владеет всеми его ресурсами, если только нет веской причины не делать этого» . Возвращаемый псевдоним может быть легально использован, но опасен в том виде, в котором вы его используете: его нельзя использовать для продления времени жизни своего «родительского» временного:
В C ++ 2a, я думаю, вы должны обойти эту (или похожую) проблему следующим образом:
вместо ОП
Временное решение вручную указывает, что время жизни
b
- это весь блок цикла for.Предложение, которое ввело это init-заявление
Live Demo
источник