Опять что-то перемудрили в стандарте или я неправильно понимаю ссылочные квалификаторы у методов класса?
Обнаружил падение у себя в коде. В сухом остатке вот выжимка. Код содержит undefined behavior.
class nested_class
{
public:
int get() const { return m_member; };
private:
int m_member = 0;
};
class holder_class
{
public:
template<typename type_t>
const nested_class& get() const& { return m_member; }
private:
nested_class m_member;
};
int main(int argc,char** argv) {
const nested_class& nested = holder_class().get<int>(); // UBreturn nested.get();
}
В оправдании себя: думал что 'const&' квалификатор метода класса убережет меня от вызова метода у временного объекта. Но не уберегло, не фортануло. Стандарт позволяет вызвать метод у временного константного объекта. Дальше масса объяснений зачем это, что ссылка константная и она продлевается и это безопасно и бла-бла-бла. Во у меня рабочий пример и таки приведение оказалось не безопасным. Понятно, что можно добавить метод
Здравствуйте, Videoman, Вы писали:
V>Опять что-то перемудрили в стандарте или я неправильно понимаю ссылочные квалификаторы у методов класса?
Проблема связана с общими правилами продления жизни временных объектов константными ссылками. А не с квалификаторами методов класса, как с очень частным случаем этого правила.
int main() {
const int& bad = std::min(5, 6);
printf("%d\n", bad); // UB
}
V> Стандарт позволяет вызвать метод у временного константного объекта.
И не только у метода — это опять же слишком частный случай.
Всегда и везде можно передавать временный объект по константной ссылке. Иначе бы такой код просто бы не компилировался:
const int good = std::min(5, 6);
Это дизайн языка, что по константной ссылке можно передать что угодно: число, строку или *this.
V>Понятно, что можно добавить метод
Не нужно, пожалуйста, добавлять такие запрещения: от них вреда больше, чем пользы. Во-первых, они не все случаи ловят, во-вторых, они запрещают множество совершенно легитимных способов использовать классы:
printf("%d\n", holder_class().get<int>().get());
В твоём случае этот код не скомпилируется, хотя никакого UB нет.
Гораздо более практичный способ избежать подобных ошибок — использовать lifetimebound-атрибут, поддержка которого уже давно есть во многих компиляторах:
error: temporary bound to local reference 'nested' will be destroyed at the end of the full-expression [-Werror,-Wdangling]
| const nested_class& nested = holder_class().get<int>();
| ^~~~~~~~~~~~~~
и при этом не получать ложных срабатываний при валидном использовании, в отличии от трюка с && = delete.
Здравствуйте, Videoman, Вы писали:
V>В оправдании себя: думал что 'const&' квалификатор метода класса убережет меня от вызова метода у временного объекта.
Если совсем на пальцах, то потому что const это совсем не то же самое, что immutable.
const совершенно не значит, что значение всегда будет хорошим, правильным и неизменяемым. Это лишь значит, что тебе менять не дадут.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Кстати, с современной STL от llvm или от microsoft в этом коде теперь проблема детектируется во время компиляции: https://godbolt.org/z/rYG74fhsz
Потому что они уже начали размечать код в STL как раз lifetimebound-атрибутами. Хотя, к сожалению, пока только начали и покрытие всё ещё не очень большое.
Здравствуйте, Videoman, Вы писали:
V>Опять что-то перемудрили в стандарте или я неправильно понимаю ссылочные квалификаторы у методов класса?
V>В оправдании себя: думал что 'const&' квалификатор метода класса убережет меня от вызова метода у временного объекта. Но не уберегло, не фортануло. Стандарт позволяет вызвать метод у временного константного объекта. Дальше масса объяснений зачем это, что ссылка константная и она продлевается и это безопасно и бла-бла-бла. Во у меня рабочий пример и таки приведение оказалось не безопасным.
V>Но почему такое поведение по умолчанию, где я ошибся в своей логике-реализации ?
Разницу между 'const' и 'const&' ты можешь обнаружить только при наличии в классе других перегрузок. Например, перегрузка по 'const' приводит к коллизии с перегрузкой по '&&', а перегрузка по 'const&' не приводит. В остальном же 'const' и 'const&' ведут себя одинаково. Т.е. обе эти перегрузки могут работать для rvalue-выражений. Т.е. правила в принципе те же самые, что и с явными параметрами обычных функций.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, ·, Вы писали:
·>Здравствуйте, Videoman, Вы писали:
V>>В оправдании себя: думал что 'const&' квалификатор метода класса убережет меня от вызова метода у временного объекта. ·>Если совсем на пальцах, то потому что const это совсем не то же самое, что immutable. ·>const совершенно не значит, что значение всегда будет хорошим, правильным и неизменяемым. Это лишь значит, что тебе менять не дадут.
Я прекрасно понимаю что такое 'const'. Вопрос был не в этом. Скорее удивление от того, что если реализовать 'const&' и 'const&&', то всё работает логично, а вот если только 'const&' то временные this спокойно делегируют все вызовы к последнему.
Спасибо за исчерпывающий ответ.
W>Не нужно, пожалуйста, добавлять такие запрещения:
Я не пишу так код в продакшене. Удивление возникло в процессе отладки нового кода, в который ещё не была добавлена реализация перегрузки для 'const&&'.
W>Гораздо более практичный способ избежать подобных ошибок — использовать lifetimebound-атрибут
Спасибо, возьму на заметку. Но пока я вынужден работать со старыми компиляторами.
Это use after free.
Взята ссылка на внутренность временного обьекта, обьект умер по выходу из области видимости, ссылка разименована.
Что сложного завести локальную переменную?
Компилятор просто недостаточно умен чтобы бить вас по рукам.
Здравствуйте, Teolog, Вы писали:
T>Это use after free. T>Взята ссылка на внутренность временного обьекта, обьект умер по выходу из области видимости, ссылка разименована. T>Что сложного завести локальную переменную? T>Компилятор просто недостаточно умен чтобы бить вас по рукам.
Здравствуйте, watchmaker, Вы писали:
W>Гораздо более практичный способ избежать подобных ошибок — использовать lifetimebound-атрибут, поддержка которого уже давно есть во многих компиляторах:
Вот это — богоугодно!
Жалко лишь, что это свойство нельзя просто так взять и протащить в систему типов... по аналогии с тем, как это есть в расте или клине.
Или можно? Хотя бы в виде какого-то костылесипеда...
template<class T> class wrapper {
const T& src_;
public:
wrapper(const T& src) : src_{src} {}
operator const T&() const [[lifetime]] { return src_; }
};
class owner {
some x_;
another y_;
.....
public:
auto x() const&& [[lifetime]] { return wrapper{x_}; }
auto y() const&& [[lifetime]] { return wrapper{y_}; }
};
Не понял, что ты хотел сделать, но wrapper написан не так.
Нужно что-то вроде этого:
template<class T> class wrapper {
const T& src_;
public:
wrapper(const T& src LIFETIMEBOUND); // wrapper не должен использоваться позже разрушения srcoperator const T&(this const wrapper self LIFETIMEBOUND); // время жизни должно следить за оригинальным src_, a не за wrapper. Ведь возвращённой ссылкой легально пользоваться, если owner ещё жив, даже если wrapper уже разрушен
};
Как видно по второй строке, у атрибута слегка неочевидные отличия в поведении в зависимости от того, применяется ли он к const wrapper или к const wrapper& (аналогично различие при применении к wrapper и к wrapper*).
Сейчас атрибут покрывает 90% случаев самых частых проблем. Но им нельзя или сложно выразить правила владения во всяких сложных структурах с умными указателями и подобным как раз из-за недостаточной выразительности. Не всегда получается просто объяснить, что нужно следить за временем жизни объекта, на который ссылается указатель, а не за самим указателем, когда эти сами указатели завёрнуты в другие указатели или ссылки.
С другой стороны, даже сейчас возможность уменьшить на 90% число банальных багов вокруг разных string_view — это уже неплохо.
Кучу кода видел, который проезжался по памяти, но выглядел при этом обманчиво нормально:
const std::string& get() {
static std::string foo = "foo";
return foo;
}
std::string_view value = cond ? get() : "default"; // "default" живёт вечно, строка с "foo" - тоже до окончания программы.
std::cout << value << '\n'; // значит string_view ссылается на живые данные?
а теперь компилятор находит в нём ошибки во время компиляции.
Здравствуйте, Videoman, Вы писали:
V> пока я вынужден работать со старыми компиляторами.
Замечу, что не нужно ждать поддержки во всех компиляторах. Достаточно одного. Или даже в статическом анализаторе (вроде clang-tidy), подключённом к проекту.
Ведь атрибуты в С++ обратно совместимы, и код под старые компиляторы продолжит собираться, даже если компилятор не понимает что с ними делать. А для репорта ошибок хватает обычно и одного источника.