diff --git a/src/libutil-tests/meson.build b/src/libutil-tests/meson.build index 9e7318bee85..5b6a616d19e 100644 --- a/src/libutil-tests/meson.build +++ b/src/libutil-tests/meson.build @@ -75,6 +75,7 @@ sources = files( 'pool.cc', 'position.cc', 'processes.cc', + 'ref.cc', 'sort.cc', 'source-accessor.cc', 'spawn.cc', diff --git a/src/libutil-tests/ref.cc b/src/libutil-tests/ref.cc new file mode 100644 index 00000000000..ed016d5cc0c --- /dev/null +++ b/src/libutil-tests/ref.cc @@ -0,0 +1,88 @@ +#include +#include + +#include "nix/util/demangle.hh" +#include "nix/util/ref.hh" + +namespace nix { + +// Test hierarchy for ref covariance tests +struct Base +{ + virtual ~Base() = default; +}; + +struct Derived : Base +{}; + +TEST(ref, upcast_is_implicit) +{ + // ref should be implicitly convertible to ref + static_assert(std::is_convertible_v, ref>); + + // Runtime test + auto derived = make_ref(); + ref base = derived; // implicit upcast + EXPECT_NE(&*base, nullptr); +} + +TEST(ref, downcast_is_rejected) +{ + // ref should NOT be implicitly convertible to ref + static_assert(!std::is_convertible_v, ref>); + + // Uncomment to see error message: + // auto base = make_ref(); + // ref d = base; +} + +TEST(ref, same_type_conversion) +{ + // ref should be convertible to ref + static_assert(std::is_convertible_v, ref>); + static_assert(std::is_convertible_v, ref>); +} + +TEST(ref, explicit_downcast_with_cast) +{ + // .cast() should work for valid downcasts at runtime + auto derived = make_ref(); + ref base = derived; + + // Downcast back to Derived using .cast() + ref backToDerived = base.cast(); + EXPECT_NE(&*backToDerived, nullptr); +} + +TEST(ref, invalid_cast_throws) +{ + // .cast() throws bad_ref_cast (a std::bad_cast subclass) with type info on invalid downcast + // (unlike .dynamic_pointer_cast() which returns nullptr) + auto base = make_ref(); + try { + base.cast(); + FAIL() << "Expected bad_ref_cast"; + } catch (const bad_ref_cast & e) { + std::string expected = "ref<" + demangle(typeid(Base).name()) + "> cannot be cast to ref<" + + demangle(typeid(Derived).name()) + ">"; + EXPECT_EQ(e.what(), expected); + } +} + +TEST(ref, explicit_downcast_with_dynamic_pointer_cast) +{ + // .dynamic_pointer_cast() returns nullptr for invalid casts + auto base = make_ref(); + + // Invalid downcast returns nullptr + auto invalidCast = base.dynamic_pointer_cast(); + EXPECT_EQ(invalidCast, nullptr); + + // Valid downcast returns non-null + auto derived = make_ref(); + ref baseFromDerived = derived; + auto validCast = baseFromDerived.dynamic_pointer_cast(); + EXPECT_NE(validCast, nullptr); +} + +} // namespace nix diff --git a/src/libutil/include/nix/util/demangle.hh b/src/libutil/include/nix/util/demangle.hh new file mode 100644 index 00000000000..997a535469e --- /dev/null +++ b/src/libutil/include/nix/util/demangle.hh @@ -0,0 +1,26 @@ +#pragma once +///@file + +#include +#include +#include + +namespace nix { + +/** + * Demangle a C++ type name. + * Returns the demangled name, or the original if demangling fails. + */ +inline std::string demangle(const char * name) +{ + int status; + char * demangled = abi::__cxa_demangle(name, nullptr, nullptr, &status); + if (demangled) { + std::string result(demangled); + std::free(demangled); + return result; + } + return name; +} + +} // namespace nix diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build index be8e603ca57..e59abec79ef 100644 --- a/src/libutil/include/nix/util/meson.build +++ b/src/libutil/include/nix/util/meson.build @@ -26,6 +26,7 @@ headers = files( 'config-impl.hh', 'configuration.hh', 'current-process.hh', + 'demangle.hh', 'english.hh', 'environment-variables.hh', 'error.hh', diff --git a/src/libutil/include/nix/util/ref.hh b/src/libutil/include/nix/util/ref.hh index 7ba5349a60b..2fc79e2167b 100644 --- a/src/libutil/include/nix/util/ref.hh +++ b/src/libutil/include/nix/util/ref.hh @@ -3,9 +3,46 @@ #include #include +#include +#include + +#include "nix/util/demangle.hh" namespace nix { +/** + * Exception thrown by ref::cast() when dynamic_pointer_cast fails. + * Inherits from std::bad_cast for semantic correctness, but carries a message with type info. + */ +class bad_ref_cast : public std::bad_cast +{ + std::string msg; + +public: + bad_ref_cast(std::string msg) + : msg(std::move(msg)) + { + } + + const char * what() const noexcept override + { + return msg.c_str(); + } +}; + +/** + * Concept for implicit ref covariance: From* must be implicitly convertible to To*. + * + * This allows implicit upcasts (Derived -> Base) but rejects downcasts. + */ +// Design note: This named concept is technically redundant but provides a readable hint +// in error messages. Alternative: static_assert can have custom messages, but doesn't +// participate in SFINAE, so std::is_convertible_v, ref> would +// incorrectly return true (the conversion would exist but fail at instantiation +// rather than being excluded). +template +concept RefImplicitlyUpcastableTo = std::is_convertible_v; + /** * A simple non-nullable reference-counted pointer. Actually a wrapper * around std::shared_ptr that prevents null constructions. @@ -76,7 +113,11 @@ public: template ref cast() const { - return ref(std::dynamic_pointer_cast(p)); + auto casted = std::dynamic_pointer_cast(p); + if (!casted) + throw bad_ref_cast( + "ref<" + demangle(typeid(T).name()) + "> cannot be cast to ref<" + demangle(typeid(T2).name()) + ">"); + return ref(std::move(casted)); } template @@ -85,10 +126,15 @@ public: return std::dynamic_pointer_cast(p); } + /** + * Implicit conversion to ref of base type (covariance). + * Downcasts are rejected; use .cast() (throws bad_ref_cast) or .dynamic_pointer_cast() (returns nullptr) instead. + */ template + requires RefImplicitlyUpcastableTo operator ref() const { - return ref((std::shared_ptr) p); + return ref(p); } bool operator==(const ref & other) const