Я хотел бы реализовать 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?
Это не работает, потому что 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
здесь.
Это эквивалентно? Он назначит конкретный тип во время компиляции. Если вы думаете иначе, покажите мне несколько документов.
Вы объясняете ОП то, что они уже знают. Они четко знают, что impl Trait
означает один тип времени компиляции, их проблема в том, что у них есть условие времени компиляции, которое должно сделать его единственным типом времени компиляции (в зависимости от результатов этой условной проверки времени компиляции), но это не так. не делаю того, что они ожидают.
@Nearoo Подробнее об универсальных и экзистенциальных типах можно прочитать здесь . В справочнике по Rust также есть раздел о черте impl. Разница в том, какая часть программы определяет тип. В fn foo() -> impl Trait
функция сама контролирует, какой тип (который подразумевает признак) возвращается. В fn foo<T: Trait>()
вместо этого вызывающая сторона решает, как разрешить T
.
Помимо недопонимания между экзистенциальными и универсальными типами в обобщенном программировании, этот ответ, похоже, предполагает, что предлагаемый C++ требует выполнения предполагаемого вывода во время выполнения, что неверно.
Хорошо, я согласен :) Скоро удалю ответ.
Можно, но это будет более запутанно, чем в 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)
}
«Это не работает, потому что fn foo() -> impl Trait — это сокращение от fn <T: Trait> foo() -> T» — это неверно;
impl Trait
эквивалентен универсальному параметру только в позиции аргумента, а не в позиции возврата.