Возвращает различную реализацию типажа на основе условия времени компиляции

Я хотел бы реализовать HList, который хранит свои элементы в отсортированном порядке. Ключ сравнения — это статическое константное значение в структурах.

У меня проблема с реализацией функции insert. В зависимости от того, где в списке должно быть вставлено новое значение, тип возвращаемого значения будет разным.

В демонстрационных целях рассмотрим суть проблемы с использованием отсортированных кортежей:

trait HasValue {
  const VALUE: i32;
}

trait SortedTuple {...}

fn make_sorted_tuple<A: HasValue, B: HasValue>(a: A, b: B) -> impl SortedTuple {
  // ILLEGAL:
  if A::VALUE < B:VALUE {
     (a, b)
  } else {
     (b, a)
  }
}

Приведенный выше код недопустим в Rust, поскольку каждая ветка должна возвращать один и тот же тип.

Однако в C++ вполне нормально возвращать разные типы из разных ветвей, если условие if можно оценить во время компиляции, как в моем случае, например:

struct Foo {
    static constexpr int value = 31;
};

struct Bar {
    static constexpr int value = 78;
};

template <class A, class B>
consteval auto make_sorted_pair(A a, B b) {
    if constexpr (A::value < B::value) {
        return std::make_pair(a, b);
    } else {
        return std::make_pair(b, a);
    }
}

int main() {
    auto a = make_sorted_pair(Bar{}, Foo{}); // a is std::pair<Foo, Bar>
}

Могу ли я добиться чего-то подобного в Rust?

Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
0
92
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это не работает, потому что fn foo() -> impl Trait — это сокращение от fn <T: Trait> foo() -> T. Таким образом, T будет конкретным типом в конце (например, (A, B)); make_sorter_pair будет компилироваться отдельно для каждого экземпляра T и, конечно же, T не может отличаться в зависимости от того, что происходит во время выполнения.

Чтобы ржавчина считала возвращаемое значение «чем-то типа Trait» без учета конкретного типа, вы можете использовать dyn Trait. Но ржавчина должна знать размер объекта во время компиляции, поэтому вы не можете вернуть dyn Trait сам по себе (который может быть реализован любым конкретным типом Trait и поэтому размер может отличаться).

Вместо этого вам придется использовать &dyn Trait, который не очень хорошо сочетается с семантикой владения, или Box<dyn Trait>, который переместит объект в кучу. Есть разные способы скрыть это многословие - например. обернув вещь в структуру - но я бы начал с Box<dyn Trait>.

Если вы хотите использовать свою черту в качестве dyn, вам необходимо следовать некоторым правилам при ее определении; например, у вас не может быть методов с параметрами типа. Попробуйте, если это соответствует вашему определению черты характера, иначе читайте больше о ключевом слове dynздесь.

«Это не работает, потому что fn foo() -> impl Trait — это сокращение от fn <T: Trait> foo() -> T» — это неверно; impl Trait эквивалентен универсальному параметру только в позиции аргумента, а не в позиции возврата.

Cerberus 19.06.2024 15:54

Это эквивалентно? Он назначит конкретный тип во время компиляции. Если вы думаете иначе, покажите мне несколько документов.

Nearoo 19.06.2024 15:56

Вы объясняете ОП то, что они уже знают. Они четко знают, что impl Trait означает один тип времени компиляции, их проблема в том, что у них есть условие времени компиляции, которое должно сделать его единственным типом времени компиляции (в зависимости от результатов этой условной проверки времени компиляции), но это не так. не делаю того, что они ожидают.

ShadowRanger 19.06.2024 16:03

@Nearoo Подробнее об универсальных и экзистенциальных типах можно прочитать здесь . В справочнике по Rust также есть раздел о черте impl. Разница в том, какая часть программы определяет тип. В fn foo() -> impl Trait функция сама контролирует, какой тип (который подразумевает признак) возвращается. В fn foo<T: Trait>() вместо этого вызывающая сторона решает, как разрешить T.

E_net4 19.06.2024 16:03

Помимо недопонимания между экзистенциальными и универсальными типами в обобщенном программировании, этот ответ, похоже, предполагает, что предлагаемый C++ требует выполнения предполагаемого вывода во время выполнения, что неверно.

E_net4 19.06.2024 16:05

Хорошо, я согласен :) Скоро удалю ответ.

Nearoo 11.07.2024 10:56
Ответ принят как подходящий

Можно, но это будет более запутанно, чем в C++ (больше похоже на C++ до constexpr). Ключевым моментом является использование системы признаков (полностью по Тьюрингу!) в качестве функций.

Для этого с помощью const требуется нестабильная функция generic_const_exprs, но вы можете сделать это в стабильной версии с помощью typenum:

use typenum::{False, Integer, IsLess, True};

// Any method you need on the tuple members will need to be on this trait.
trait HasValue {
    type Value: Integer;
}

type Value<T> = <T as HasValue>::Value;
type ValueIsLess<A, B> = <Value<A> as IsLess<Value<B>>>::Output;
type SortedTuple<A, B> = (
    <ValueIsLess<A, B> as MakeSortedTuple<A, B>>::First,
    <ValueIsLess<A, B> as MakeSortedTuple<A, B>>::Second,
);

trait MakeSortedTuple<A, B> {
    type First: HasValue;
    type Second: HasValue;
    fn new(a: A, b: B) -> (Self::First, Self::Second);
}

impl<A: HasValue, B: HasValue> MakeSortedTuple<A, B> for True {
    type First = A;
    type Second = B;
    fn new(a: A, b: B) -> (Self::First, Self::Second) {
        (a, b)
    }
}

impl<A: HasValue, B: HasValue> MakeSortedTuple<A, B> for False {
    type First = B;
    type Second = A;
    fn new(a: A, b: B) -> (Self::First, Self::Second) {
        (b, a)
    }
}

fn make_sorted_tuple<A, B>(a: A, b: B) -> SortedTuple<A, B>
// Yep, this is a monster.
// It's because Rust, unlike C++, requires you to declare all trait bounds upfront.
where
    A: HasValue,
    B: HasValue,
    Value<A>: IsLess<Value<B>>,
    ValueIsLess<A, B>: MakeSortedTuple<A, B>,
{
    <ValueIsLess<A, B> as MakeSortedTuple<A, B>>::new(a, b)
}

Другие вопросы по теме