Почему можно сравнить два, казалось бы, равных указателя с символом == return false?

Я хочу проверить, содержат ли два объекта типа Rc<Trait> тот же экземпляр конкретного типа, поэтому я сравниваю указатели с объектами внутри Rc на равенство. Кажется, он работает правильно, если весь код находится в одном и том же ящике, но не работает, когда задействованы несколько ящиков.

В Rust 1.17 была добавлена функция Rc::ptr_eq, которая Rc::ptr_eq с Rust 1.31 демонстрирует ту же проблему кросс-ящиков, что и ручное сравнение указателей, использованное в этом вопросе.

Это реализация crate mcve (src/lib.rs):

use std::rc::Rc;

pub trait ObjectInterface {}

pub type Object = Rc<ObjectInterface>;

pub type IntObject = Rc<i32>;

impl ObjectInterface for i32 {}

/// Test if two Objects refer to the same instance
pub fn is_same(left: &Object, right: &Object) -> bool {
    let a = left.as_ref() as *const _;
    let b = right.as_ref() as *const _;
    let r = a == b;
    println!("comparing: {:p} == {:p} -> {}", a, b, r);
    r
}

pub struct Engine {
    pub intval: IntObject,
}

impl Engine {
    pub fn new() -> Engine {
        Engine {
            intval: Rc::new(42),
        }
    }

    pub fn run(&mut self) -> Object {
        return self.intval.clone();
    }
}

Я тестирую реализацию с помощью следующего кода (tests/testcases.rs):

extern crate mcve;

use mcve::{is_same, Engine, Object};

#[test]
fn compare() {
    let mut engine = Engine::new();

    let a: Object = engine.intval.clone();
    let b = a.clone();
    assert!(is_same(&a, &b));

    let r = engine.run();
    assert!(is_same(&r, &a));
}

Выполнение теста приводит к следующему выводу:

comparing: 0x7fcc5720d070 == 0x7fcc5720d070 -> true
comparing: 0x7fcc5720d070 == 0x7fcc5720d070 -> false
thread 'compare' panicked at 'assertion failed: is_same(&r, &a)'

Как это возможно, что оператор сравнения == возвращает false хотя указатели кажутся одинаковыми?

Несколько замечаний:

  • Сравнение возвращает true когда оба объекта (a и b) живут в одном и том же ящике. Однако сравнение возвращает false когда один из объектов (r) был возвращен функцией Engine::run, которая определена в другом ящике.
  • Тест корректно проходит, когда я помещаю тестовую функцию в lib.rs
  • Проблема может быть решена путем определения struct Engine { intval: Object }, но меня все еще интересует, почему.

Ответы

Ответ 1

Когда "указатель" не является "указателем"? Когда это жирный указатель. ObjectInterface является признаком, что означает, что &dyn ObjectInterface является объектом признака. Объекты признаков состоят из двух машинных указателей: один для конкретных данных и один для vtable, набор конкретных реализаций признака для конкретного значения. Этот двойной указатель называется толстым указателем.

Используя ночной компилятор и std::raw::TraitObject, вы можете увидеть различия:

#![feature(raw)]

use std::{mem, raw};

pub fn is_same(left: &Object, right: &Object) -> bool {
    let a = left.as_ref() as *const _;
    let b = right.as_ref() as *const _;
    let r = a == b;
    println!("comparing: {:p} == {:p} -> {}", a, b, r);

    let raw_object_a: raw::TraitObject = unsafe { mem::transmute(left.as_ref()) };
    let raw_object_b: raw::TraitObject = unsafe { mem::transmute(right.as_ref()) };
    println!(
        "really comparing: ({:p}, {:p}) == ({:p}, {:p})",
        raw_object_a.data, raw_object_a.vtable,
        raw_object_b.data, raw_object_b.vtable,
    );

    r
}
comparing: 0x101c0e010 == 0x101c0e010 -> true
really comparing: (0x101c0e010, 0x1016753e8) == (0x101c0e010, 0x1016753e8)
comparing: 0x101c0e010 == 0x101c0e010 -> false
really comparing: (0x101c0e010, 0x101676758) == (0x101c0e010, 0x1016753e8)

Оказывается, что (по крайней мере в Rust 1.22.1) каждый модуль генерации кода создает отдельный vtable! Это объясняет, почему это работает, когда все это в одном модуле. Там активно обсуждают, если это ошибка или нет.

Когда вы аннотируете new и run функции с помощью #[inline] потребители будут использовать эту vtable.


Как сказал Фрэнсис Гань:

Вы можете изменить as *const _ на as *const _ as *const() чтобы превратить толстый указатель в обычный указатель, если вам важен только адрес значения.

Это может быть четко выражено с использованием std::ptr::eq:

use std::ptr;

pub fn is_same(left: &Object, right: &Object) -> bool {
    let r = ptr::eq(left.as_ref(), right.as_ref());
    println!("comparing: {:p} == {:p} -> {}", left, right, r);
    r
}