Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libutil-tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ sources = files(
'pool.cc',
'position.cc',
'processes.cc',
'ref.cc',
'sort.cc',
'source-accessor.cc',
'spawn.cc',
Expand Down
88 changes: 88 additions & 0 deletions src/libutil-tests/ref.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include <gtest/gtest.h>
#include <type_traits>

#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<Derived> should be implicitly convertible to ref<Base>
static_assert(std::is_convertible_v<ref<Derived>, ref<Base>>);

// Runtime test
auto derived = make_ref<Derived>();
ref<Base> base = derived; // implicit upcast
EXPECT_NE(&*base, nullptr);
}

TEST(ref, downcast_is_rejected)
{
// ref<Base> should NOT be implicitly convertible to ref<Derived>
static_assert(!std::is_convertible_v<ref<Base>, ref<Derived>>);

// Uncomment to see error message:
// auto base = make_ref<Base>();
// ref<Derived> d = base;
}

TEST(ref, same_type_conversion)
{
// ref<T> should be convertible to ref<T>
static_assert(std::is_convertible_v<ref<Base>, ref<Base>>);
static_assert(std::is_convertible_v<ref<Derived>, ref<Derived>>);
}

TEST(ref, explicit_downcast_with_cast)
{
// .cast() should work for valid downcasts at runtime
auto derived = make_ref<Derived>();
ref<Base> base = derived;

// Downcast back to Derived using .cast()
ref<Derived> backToDerived = base.cast<Derived>();
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<Base>();
try {
base.cast<Derived>();
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<Base>();

// Invalid downcast returns nullptr
auto invalidCast = base.dynamic_pointer_cast<Derived>();
EXPECT_EQ(invalidCast, nullptr);

// Valid downcast returns non-null
auto derived = make_ref<Derived>();
ref<Base> baseFromDerived = derived;
auto validCast = baseFromDerived.dynamic_pointer_cast<Derived>();
EXPECT_NE(validCast, nullptr);
}

} // namespace nix
26 changes: 26 additions & 0 deletions src/libutil/include/nix/util/demangle.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once
///@file

#include <cstdlib>
#include <cxxabi.h>
#include <string>

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
1 change: 1 addition & 0 deletions src/libutil/include/nix/util/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ headers = files(
'config-impl.hh',
'configuration.hh',
'current-process.hh',
'demangle.hh',
'english.hh',
'environment-variables.hh',
'error.hh',
Expand Down
50 changes: 48 additions & 2 deletions src/libutil/include/nix/util/ref.hh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,46 @@

#include <memory>
#include <stdexcept>
#include <string>
#include <typeinfo>

#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<Base>, ref<Derived>> would
// incorrectly return true (the conversion would exist but fail at instantiation
// rather than being excluded).
template<typename From, typename To>
concept RefImplicitlyUpcastableTo = std::is_convertible_v<From *, To *>;

/**
* A simple non-nullable reference-counted pointer. Actually a wrapper
* around std::shared_ptr that prevents null constructions.
Expand Down Expand Up @@ -76,7 +113,11 @@ public:
template<typename T2>
ref<T2> cast() const
{
return ref<T2>(std::dynamic_pointer_cast<T2>(p));
auto casted = std::dynamic_pointer_cast<T2>(p);
if (!casted)
throw bad_ref_cast(
"ref<" + demangle(typeid(T).name()) + "> cannot be cast to ref<" + demangle(typeid(T2).name()) + ">");
return ref<T2>(std::move(casted));
}

template<typename T2>
Expand All @@ -85,10 +126,15 @@ public:
return std::dynamic_pointer_cast<T2>(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<typename T2>
requires RefImplicitlyUpcastableTo<T, T2>
operator ref<T2>() const
{
return ref<T2>((std::shared_ptr<T2>) p);
return ref<T2>(p);
}

bool operator==(const ref<T> & other) const
Expand Down
Loading