blob: 6dff5da3f8c47ef3882ad6030ae6b51bbc400ab1 [file] [edit]
// Part of the Crubit project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include <type_traits>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wglobal-constructors"
#include <utility>
#include "support/internal/slot.h"
#include "gtest/gtest.h"
#include "support/movable.h"
namespace {
// A normal C++ movable type.
struct CppMovable {
int value;
explicit CppMovable(int v) : value(v) {}
CppMovable(CppMovable&& other) : value(other.value) { other.value = -1; }
CppMovable& operator=(CppMovable&& other) {
value = other.value;
other.value = -1;
return *this;
}
CppMovable(const CppMovable&) = delete;
CppMovable& operator=(const CppMovable&) = delete;
};
// A type that is NOT C++ movable, but has UnsafeRelocateTag constructor.
struct RustMovable {
int value;
bool relocated_from = false;
static inline int relocate_calls = 0;
static inline int destructor_calls = 0;
explicit RustMovable(int v) : value(v) {}
~RustMovable() {
destructor_calls++;
// If it was relocated from, we shouldn't be destroying it (except if it was
// a temporary, but in our Slot/Value tests we should avoid destroying
// relocated-from objects).
if (relocated_from) {
// In a real app, this might crash or be a no-op depending on how
// "defused" it is. For test, we can check if we shouldn't have been
// destroyed. But we can't easily assert here without failing the test if
// C++ forces destructor. However, Slot and Value should prevent
// destructor from running on relocated-from.
}
}
// No C++ move/copy
RustMovable(RustMovable&&) = delete;
RustMovable& operator=(RustMovable&&) = delete;
RustMovable(const RustMovable&) = delete;
RustMovable& operator=(const RustMovable&) = delete;
// Rust move constructor
RustMovable(crubit::UnsafeRelocateTag, RustMovable&& other)
: value(other.value) {
other.relocated_from = true;
relocate_calls++;
}
};
struct DefaultConstructible {
int value = 42;
};
TEST(MovableTest, DefaultConstructor) {
static_assert(
std::is_default_constructible_v<rs::Movable<DefaultConstructible>>);
static_assert(!std::is_default_constructible_v<rs::Movable<CppMovable>>);
rs::Movable<DefaultConstructible> v;
EXPECT_FALSE(v.valueless_after_move());
EXPECT_TRUE(v);
EXPECT_EQ(v->value, 42);
}
TEST(MovableTest, ConstructFromValue) {
rs::Movable<CppMovable> v(CppMovable(42));
EXPECT_FALSE(v.valueless_after_move());
EXPECT_EQ(v->value, 42);
}
TEST(MovableTest, ConstructInPlace) {
rs::Movable<CppMovable> v(std::in_place, 42);
EXPECT_FALSE(v.valueless_after_move());
EXPECT_EQ(v->value, 42);
}
TEST(MovableTest, MoveNormalType) {
rs::Movable<CppMovable> v1(CppMovable(42));
rs::Movable<CppMovable> v2(std::move(v1));
EXPECT_FALSE(v2.valueless_after_move());
EXPECT_EQ(v2->value, 42);
EXPECT_TRUE(v1.valueless_after_move()); // NOLINT(bugprone-use-after-move)
EXPECT_FALSE(v1); // NOLINT(bugprone-use-after-move)
}
TEST(MovableTest, ConstructFromSlotRustMovable) {
static_assert(!std::is_constructible_v<rs::Movable<RustMovable>,
crubit::Slot<RustMovable>&&>);
RustMovable::relocate_calls = 0;
RustMovable::destructor_calls = 0;
{
crubit::Slot<RustMovable> slot;
new (slot.Get()) RustMovable(42);
auto v = rs::Movable<RustMovable>::TakeFromSlot(std::move(slot));
EXPECT_FALSE(v.valueless_after_move());
EXPECT_EQ(v->value, 42);
EXPECT_EQ(RustMovable::relocate_calls, 1);
EXPECT_EQ(RustMovable::destructor_calls,
0); // Slot destructor shouldn't destroy it
}
// v went out of scope, should destroy RustMovable
EXPECT_EQ(RustMovable::destructor_calls, 1);
}
TEST(MovableTest, MoveRustMovable) {
RustMovable::relocate_calls = 0;
RustMovable::destructor_calls = 0;
{
crubit::Slot<RustMovable> slot;
new (slot.Get()) RustMovable(42);
auto v1 = rs::Movable<RustMovable>::TakeFromSlot(std::move(slot));
EXPECT_EQ(RustMovable::relocate_calls, 1);
rs::Movable<RustMovable> v2(std::move(v1));
EXPECT_FALSE(v2.valueless_after_move());
EXPECT_EQ(v2->value, 42);
EXPECT_TRUE(v1.valueless_after_move()); // NOLINT(bugprone-use-after-move)
EXPECT_FALSE(v1); // NOLINT(bugprone-use-after-move)
EXPECT_EQ(RustMovable::relocate_calls, 2);
EXPECT_EQ(RustMovable::destructor_calls,
0); // v1 should not destroy its value
}
// v2 went out of scope
EXPECT_EQ(RustMovable::destructor_calls, 1);
}
TEST(MovableTest, MoveAssignmentRustMovable) {
RustMovable::relocate_calls = 0;
RustMovable::destructor_calls = 0;
{
crubit::Slot<RustMovable> slot1;
new (slot1.Get()) RustMovable(42);
auto v1 = rs::Movable<RustMovable>::TakeFromSlot(std::move(slot1));
crubit::Slot<RustMovable> slot2;
new (slot2.Get()) RustMovable(100);
auto v2 = rs::Movable<RustMovable>::TakeFromSlot(std::move(slot2));
EXPECT_EQ(RustMovable::relocate_calls, 2);
EXPECT_EQ(RustMovable::destructor_calls, 0);
v2 = std::move(
v1); // should destroy v2's old value, and relocate v1 into v2
EXPECT_FALSE(v2.valueless_after_move());
EXPECT_EQ(v2->value, 42);
EXPECT_TRUE(v1.valueless_after_move()); // NOLINT(bugprone-use-after-move)
EXPECT_FALSE(v1); // NOLINT(bugprone-use-after-move)
EXPECT_EQ(RustMovable::relocate_calls, 3);
EXPECT_EQ(RustMovable::destructor_calls,
1); // v2's old value (100) destroyed
}
// v2 went out of scope (contains 42)
EXPECT_EQ(RustMovable::destructor_calls, 2);
}
TEST(MovableTest, MoveToSlot) {
RustMovable::relocate_calls = 0;
RustMovable::destructor_calls = 0;
{
crubit::Slot<RustMovable> slot1;
new (slot1.Get()) RustMovable(42);
auto v = rs::Movable<RustMovable>::TakeFromSlot(std::move(slot1));
EXPECT_EQ(RustMovable::relocate_calls, 1);
EXPECT_EQ(RustMovable::destructor_calls, 0);
crubit::Slot<RustMovable> slot2;
std::move(v).MoveToSlot(slot2);
EXPECT_TRUE(v.valueless_after_move()); // NOLINT(bugprone-use-after-move)
EXPECT_EQ(RustMovable::relocate_calls, 2);
EXPECT_EQ(RustMovable::destructor_calls, 0);
// Take out of slot2 and destroy
auto v2 = rs::Movable<RustMovable>::TakeFromSlot(std::move(slot2));
EXPECT_EQ(RustMovable::relocate_calls, 3);
EXPECT_EQ(RustMovable::destructor_calls, 0);
}
EXPECT_EQ(RustMovable::destructor_calls, 1);
}
struct Copyable {
int value;
};
TEST(MovableTest, CopySemantics) {
static_assert(std::is_copy_constructible_v<rs::Movable<Copyable>>);
static_assert(std::is_copy_assignable_v<rs::Movable<Copyable>>);
static_assert(!std::is_copy_constructible_v<rs::Movable<CppMovable>>);
static_assert(!std::is_copy_constructible_v<rs::Movable<RustMovable>>);
rs::Movable<Copyable> v1(Copyable{42});
rs::Movable<Copyable> v2(v1);
EXPECT_FALSE(v1.valueless_after_move());
EXPECT_FALSE(v2.valueless_after_move());
EXPECT_EQ(v1->value, 42);
EXPECT_EQ(v2->value, 42);
rs::Movable<Copyable> v3(Copyable{100});
v3 = v1;
EXPECT_FALSE(v3.valueless_after_move());
EXPECT_EQ(v3->value, 42);
}
rs::Movable<RustMovable> MakeRustMovable(int value) {
crubit::Slot<RustMovable> slot;
new (slot.Get()) RustMovable(value);
return rs::Movable<RustMovable>::TakeFromSlot(std::move(slot));
}
int ReadRustMovable(const RustMovable& v) { return v.value; }
TEST(MovableTest, ImplicitConversionToValue) {
// Only rvalues convert, so that an lvalue is never silently emptied.
static_assert(std::is_convertible_v<rs::Movable<RustMovable>, RustMovable>);
static_assert(std::is_convertible_v<rs::Movable<CppMovable>, CppMovable>);
static_assert(!std::is_convertible_v<rs::Movable<RustMovable>&, RustMovable>);
static_assert(
!std::is_convertible_v<const rs::Movable<RustMovable>&, RustMovable>);
RustMovable::relocate_calls = 0;
RustMovable::destructor_calls = 0;
{
// Copy-initialization from a prvalue `Movable<T>` initializes the `T`
// directly, even though `T` has no C++ move constructor.
RustMovable v = MakeRustMovable(42);
EXPECT_EQ(v.value, 42);
// One relocation into the `Movable`, and one out of it. The emptied
// `Movable` temporary did not run `T`'s destructor.
EXPECT_EQ(RustMovable::relocate_calls, 2);
EXPECT_EQ(RustMovable::destructor_calls, 0);
}
EXPECT_EQ(RustMovable::destructor_calls, 1);
}
TEST(MovableTest, ImplicitConversionBindsToConstRef) {
EXPECT_EQ(ReadRustMovable(MakeRustMovable(42)), 42);
}
TEST(MovableTest, ImplicitConversionWithLifetimeExtension) {
// `const T& x = f();` binds `x` to the `T` produced by the conversion
// function, which is a temporary bound directly to the reference and so
// lives as long as `x`. The `Movable` temporary dies at the end of the
// full-expression, but it is valueless by then and does not destroy the `T`.
RustMovable::relocate_calls = 0;
RustMovable::destructor_calls = 0;
{
const RustMovable& x = MakeRustMovable(42);
EXPECT_EQ(x.value, 42);
EXPECT_EQ(RustMovable::relocate_calls, 2);
EXPECT_EQ(RustMovable::destructor_calls, 0);
RustMovable&& y = MakeRustMovable(7);
EXPECT_EQ(y.value, 7);
EXPECT_EQ(RustMovable::destructor_calls, 0);
}
EXPECT_EQ(RustMovable::destructor_calls, 2);
}
TEST(MovableTest, ImplicitConversionFromLvalueWithStdMove) {
RustMovable::relocate_calls = 0;
RustMovable::destructor_calls = 0;
{
rs::Movable<RustMovable> m = MakeRustMovable(42);
RustMovable v = std::move(m);
EXPECT_EQ(v.value, 42);
EXPECT_TRUE(m.valueless_after_move()); // NOLINT(bugprone-use-after-move)
EXPECT_FALSE(m); // NOLINT(bugprone-use-after-move)
EXPECT_EQ(RustMovable::destructor_calls, 0);
}
// Only `v` is destroyed; the valueless `m` is not.
EXPECT_EQ(RustMovable::destructor_calls, 1);
}
TEST(MovableTest, ImplicitConversionOfCppMovable) {
rs::Movable<CppMovable> m(std::in_place, 42);
CppMovable v = std::move(m);
EXPECT_EQ(v.value, 42);
EXPECT_TRUE(m.valueless_after_move()); // NOLINT(bugprone-use-after-move)
}
TEST(MovableDeathTest, ImplicitConversionOfValuelessAborts) {
rs::Movable<CppMovable> m(std::in_place, 42);
CppMovable v = std::move(m);
EXPECT_EQ(v.value, 42);
EXPECT_DEATH(
{
CppMovable again = std::move(m); // NOLINT(bugprone-use-after-move)
(void)again;
},
"valueless");
}
} // namespace
#pragma clang diagnostic pop