Я хочу разработать черту с изменяемым и неизменяемым геттером для некоторого поля self
.
Однако я хочу, чтобы разработчик трейта реализовал только один метод, как правило, изменяемый геттер.
Я нашел это решение (детская площадка):
pub trait MyTrait<T> {
fn inside_mut(&mut self) -> &mut T;
fn inside_ref(&self) -> &T {
let ptr = self as *const Self as *mut Self;
&* unsafe { &mut *ptr }.inside_mut()
}
}
pub struct Data(String);
impl MyTrait<String> for Data {
fn inside_mut(&mut self) -> &mut String { &mut self.0 }
}
fn main() {
let mut data = Data(format!("Foo"));
let x: &String = data.inside_ref();
let y: &String = data.inside_ref();
println!("x -> {x}");
println!("y -> {y}");
*data.inside_mut() = format!("Bar");
println!("data.0 -> {}", data.0);
}
Результат:
Standard Error
Compiling playground v0.0.1 (/playground)
Finished release [optimized] target(s) in 0.93s
Running `target/release/playground`
Standard Output
x -> Foo
y -> Foo
data.0 -> Bar
Мой вопрос: этот подход звучит?
Техническое замечание: Вы, наверное, спрашиваете, является ли такой подход разумным, небезопасным.
Точно! Я отредактировал вопрос.
Нет, это не звук.
Пользователь может изменить self
в своей реализации inside_mut
, что также изменит структуру в inside_ref
.
Так:
pub trait MyTrait<T> {
fn inside_mut(&mut self) -> &mut T;
fn inside_ref(&self) -> &T {
let ptr = self as *const Self as *mut Self;
&*unsafe { (&mut *ptr).inside_mut() }
}
}
#[derive(Debug)]
struct MyStruct {
value: i32,
}
impl MyTrait<i32> for MyStruct {
fn inside_mut(&mut self) -> &mut i32 {
self.value += 1;
&mut self.value
}
}
fn main() {
// Important: this is not `mut`!!!
let x = MyStruct { value: 42 };
println!("{:?}", x);
x.inside_ref();
println!("{:?}", x);
}
MyStruct { value: 42 }
MyStruct { value: 43 }
Это означает, что эта реализация несостоятельна:
Поведение считается неопределенным
- Изменение неизменяемых данных. Все данные внутри элемента
const
неизменяемы. Более того, все данные, полученные через общую ссылку или данные, принадлежащие неизменяемой привязке, являются неизменяемыми, если только эти данные не содержатся вUnsafeCell<U>
.
О, да! Я не заметил этой тонкости!
Даже если пользователь этого не делает, это немедленный UB, поскольку преобразование общей ссылки в изменяемую ссылку является немедленным UB, даже если она не изменена.
Посмотрев на ответ Finomnis, я пришел к этому решению (площадка), для которого miri не выдает предупреждение UB:
use core::cell::UnsafeCell;
pub trait MyTrait<T> {
fn inside_mut(&mut self) -> &mut T {
let ptr = self.implement_ref().get();
unsafe { &mut *ptr }
}
fn inside_ref(&self) -> &T {
let ptr = self.implement_ref().get();
unsafe { & *ptr }
}
fn implement_ref(&self) -> &UnsafeCell<T>;
}
pub struct Data(UnsafeCell<String>);
impl Data {
pub fn new(s:String) -> Self {
Data(UnsafeCell::new(s))
}
}
impl MyTrait<String> for Data {
fn implement_ref(&self) -> &UnsafeCell<String> { &self.0 }
}
fn main() {
let mut data = Data::new(format!("Foo"));
let x: &String = data.inside_ref();
let y: &String = data.inside_ref();
println!("x -> {x}");
println!("y -> {y}");
*data.inside_mut() = format!("Bar");
println!("data.0 -> {}", data.inside_ref());
}
Тем не менее, у меня все еще есть вопрос. Мири выдает UB-алерт для следующего кода (площадка):
pub trait MyTrait<T> {
fn inside_ref(&self) -> &T;
fn inside_mut(&mut self) -> &mut T {
let ptr = self.inside_ref() as *const T as *mut T;
unsafe { &mut *ptr }
}
}
pub struct Data(String);
impl MyTrait<String> for Data {
fn inside_ref(&self) -> &String { &self.0 }
}
fn main() {
let mut data = Data(format!("Foo"));
let x: &String = data.inside_ref();
let y: &String = data.inside_ref();
println!("x -> {x}");
println!("y -> {y}");
*data.inside_mut() = format!("Bar");
println!("data.0 -> {}", data.0);
}
Но хороший контрпример, представленный Finomnis, в этом случае не работает. Может ли кто-нибудь привести иллюстрацию несостоятельности последнего случая?
Обновлено: вдохновлено контрпримером cafce25 (детская площадка):
static IMMUTABLE_STATIC_STR: &'static str = "IMMUTABLE FOO";
pub trait MyTrait<T> {
fn inside_ref(&self) -> &T;
fn inside_mut(&mut self) -> &mut T {
let ptr = self.inside_ref() as *const T as *mut T;
unsafe { &mut *ptr }
}
}
pub struct Data;
impl MyTrait<&'static str> for Data {
fn inside_ref(&self) -> &&'static str { &IMMUTABLE_STATIC_STR }
}
fn main() {
let mut data = Data;
let x: &&'static str = data.inside_ref();
let y: &&'static str = data.inside_ref();
println!("x -> {x}");
println!("y -> {y}");
*data.inside_mut() = "Bar";
println!("data.inside_ref() -> {}", data.inside_ref());
}
Результат:
Standard Error
Compiling playground v0.0.1 (/playground)
Finished release [optimized] target(s) in 1.61s
Running `target/release/playground`
Standard Output
x -> IMMUTABLE FOO
y -> IMMUTABLE FOO
data.inside_ref() -> IMMUTABLE FOO
Во-первых, для нового вопроса, пожалуйста, задайте новый вопрос SO. Во-вторых, UnsafeCell
работает, но это плохая идея. Гораздо лучше иметь два метода для реализации.
Мири не проверяет (и не может) исправность. Он только проверяет, содержит ли одно выполнение вашей программы UB.
Все в порядке! Я не думаю, что новый вопрос стоит того. И спасибо за ваш совет!
Я согласен с вами, что следует избегать небезопасного подхода. Но, безусловно, бывают случаи, когда дизайнер признаков хочет быть уверенным, что оба метода относятся к одним и тем же данным. Тем не менее, проблемы в любом случае можно избежать, реализуя только изменяемый геттер и работая с индуцированным ограничением.
fn inside_ref(&self) -> &T { &IMMUTABLE_STATIC_T }
угу.
Большой! Мне еще многому нужно научиться, прежде чем я смогу с такой легкостью приводить контрпримеры.
@ChayimFriedman Я не понимаю, почему пример так вас раздражает, ОП не понимал, почему это плохая идея. Пример говорит о вас более чем на 1000 слов, что это немедленный UB
@ cafce25 Это плохая идея, потому что это UB во всех случаях и примерах.
@ChayimFriedman Да, конечно, я не понимаю, почему пример, помогающий понять ОП, - это плохо, о чем нужно разглагольствовать.
@ cafce25 Это может заставить ОП (и я думаю, что это заставило их) думать, что только когда мы находим контрпример, это плохо.
@ChayimFriedman Я задал этот вопрос, потому что знал, что это решение с изменяемым преобразованием ссылок было неправильным, и я хотел знать, как избежать UB. Что касается меня, то мне не нужен контрпример, чтобы убедить меня не использовать УБ. Однако мне придется познакомить с ржавчиной некоторых программистов на C/C++, и в этом контексте мне будут полезны контрпримеры. У меня уже есть контрпримеры UB, подразумеваемые оптимизацией компилятора. Но примеры в этом посте другие, потому что они являются следствием языка. Для меня очень интересно это увидеть.
Не эксперт по небезопасной ржавчине, но нет, это не кажется безопасным, поскольку мири обнаруживает UB. Вы можете запустить Мири на детской площадке. Вы можете найти эту опцию в меню инструментов в правом верхнем углу.