Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
46 changes: 38 additions & 8 deletions include/iris/type_traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,43 @@ template<class T>
constexpr bool is_trivially_swappable_v = is_trivially_swappable<T>::value;


// P0870R7: is_convertible_without_narrowing
// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p0870r7.html
template<class From, class To>
struct is_convertible_without_narrowing
: std::false_type
{};

template<class From, class To>
requires
std::is_convertible_v<From, To> &&
requires { std::type_identity_t<To[]>{std::declval<From>()}; }
struct is_convertible_without_narrowing<From, To>
: std::true_type
{};

template<class From, class To>
inline constexpr bool is_convertible_without_narrowing_v = is_convertible_without_narrowing<From, To>::value;


// is_assignable_without_narrowing<Dest, Source>
//
// True when `Dest = Source` is valid AND does not involve a narrowing conversion.
// For non-arithmetic Dest types, narrowing is not checked — we trust that the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non-arithmetic Dest types, narrowing is not checked

Why? This doesn't make sense at all.

"—" is a very bad sign for vibe coded slop. Please, please double check both the description and code itself.

Do you really think "not is arithmetic" can be the constituent part for being "assignable without narrowing"?

Arithmeric types are just arithmeric types. There should be absolutely no direct relationship to the concept of "assignable", whatever the condition is.

Am I missing something?

I'm really disappointed to see this level of low quality code.

@yaito3014 yaito3014 Mar 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in the clarification, is_assignable_without_narrowing itself was for detecting narrowing conversion, so it was just is_assignable for non-arithmetic types. The "mistake" was that the trait was used directly in X4 codebase, not the trait definition.
I admit that the clarification was missing, but before saying "vibe coded slop" or "low quality code", rethink about it.

// user-defined conversion handles the assignment correctly.
template<class Dest, class Source>
struct is_assignable_without_narrowing
: std::bool_constant<
std::is_assignable_v<Dest, Source> &&
(!std::is_arithmetic_v<std::remove_reference_t<Dest>> ||
is_convertible_without_narrowing_v<std::remove_cvref_t<Source>, std::remove_reference_t<Dest>>)
>
{};

template<class Dest, class Source>
inline constexpr bool is_assignable_without_narrowing_v = is_assignable_without_narrowing<Dest, Source>::value;


namespace detail {

template<std::size_t I, class Ti>
Expand All @@ -231,19 +268,12 @@ struct aggregate_initialize_tag
using type = Ti;
};

// This version works better than MSVC's, does not break IntelliSense or ReSharper
template<std::size_t I, class Ti>
struct aggregate_initialize_overload
Comment thread
yaito3014 marked this conversation as resolved.
Outdated
{
using TiA = Ti[];

// https://eel.is/c++draft/dcl.init.general#14
// https://eel.is/c++draft/dcl.init.list#3.4
// https://eel.is/c++draft/dcl.init.aggr#3

template<class T>
auto operator()(Ti, T&&) -> aggregate_initialize_tag<I, Ti>
requires requires(T&& t) { { TiA{std::forward<T>(t)} }; } // emulate `Ti x[] = {std::forward<T>(t)};`
requires is_convertible_without_narrowing_v<T, Ti>
Comment on lines 277 to +279

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the first parameter Ti can be omitted, as it existed for checking is_convertible by passing-by-value

@yaito3014 yaito3014 Mar 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the Ti parameter cannot be omitted, since we should consider overload resolution for Ti including implicit conversion sequence's priority.

For example, constructing rvariant<int, long> from int should be resolved to index() == 0 though both int and long are equally convertible from int without narrowing. This is because identity conversion should occur prior to integral promotion in standard conversion.

{
return {}; // silence MSVC warning
}
Expand Down
130 changes: 130 additions & 0 deletions test/type_traits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,139 @@ struct n_tuple;
template<int... Ns>
struct n_list;

struct convertible_from_int
{
convertible_from_int(int);
};

struct not_convertible_from_int {};

enum class scoped_enum {};
enum unscoped_enum {};

} // anonymous


TEST_CASE("is_convertible_without_narrowing")
{
// Same type: always OK
STATIC_CHECK(iris::is_convertible_without_narrowing_v<int, int>);
STATIC_CHECK(iris::is_convertible_without_narrowing_v<double, double>);

// Widening integer conversions: OK
STATIC_CHECK(iris::is_convertible_without_narrowing_v<int, long long>);
STATIC_CHECK(iris::is_convertible_without_narrowing_v<short, int>);
STATIC_CHECK(iris::is_convertible_without_narrowing_v<char, int>);
STATIC_CHECK(iris::is_convertible_without_narrowing_v<unsigned, unsigned long long>);

// Narrowing integer conversions: rejected
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<long long, int>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, short>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, char>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<unsigned long long, unsigned>);

// Floating-point widening: OK
STATIC_CHECK(iris::is_convertible_without_narrowing_v<float, double>);
STATIC_CHECK(iris::is_convertible_without_narrowing_v<float, long double>);
STATIC_CHECK(iris::is_convertible_without_narrowing_v<double, long double>);

// Floating-point narrowing: rejected
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<double, float>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<long double, double>);

// Integer to floating-point: narrowing
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, float>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<long long, double>);

// Floating-point to integer: narrowing
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<float, int>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<double, int>);

// Signed/unsigned mismatch
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, unsigned>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<unsigned, int>);

// Bool conversions
STATIC_CHECK(iris::is_convertible_without_narrowing_v<bool, int>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, bool>);

// User-defined implicit conversion: OK (no narrowing concern)
STATIC_CHECK(iris::is_convertible_without_narrowing_v<int, convertible_from_int>);

// Not convertible at all
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, not_convertible_from_int>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, std::string>);

// Enum conversions
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, scoped_enum>);
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<scoped_enum, int>);
STATIC_CHECK(iris::is_convertible_without_narrowing_v<unscoped_enum, int>);

// `To[]{from}` works, but not convertible
{
{
struct S {
union {
int x;
float y;
} u;
};
[[maybe_unused]] S s{42};
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, S>);
}
{
struct S {
int x[1];
};
[[maybe_unused]] S s{42};
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, S>);
}
{
struct S {
struct {
int x;
} inner;
};
[[maybe_unused]] S s{42};
STATIC_CHECK(!iris::is_convertible_without_narrowing_v<int, S>);
}
}

}


TEST_CASE("is_assignable_without_narrowing")
{
// Non-narrowing: same type
STATIC_CHECK(iris::is_assignable_without_narrowing_v<int&, int>);
STATIC_CHECK(iris::is_assignable_without_narrowing_v<double&, double>);

// Non-narrowing: widening
STATIC_CHECK(iris::is_assignable_without_narrowing_v<long long&, int>);
STATIC_CHECK(iris::is_assignable_without_narrowing_v<double&, float>);
STATIC_CHECK(iris::is_assignable_without_narrowing_v<int&, short>);

// Narrowing: lossy conversions
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<int&, long long>);
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<short&, int>);
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<float&, double>);
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<int&, float>);
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<float&, int>);

// Signed/unsigned mismatch
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<int&, unsigned>);
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<unsigned&, int>);

// Non-arithmetic dest: narrowing not checked (trusts user-defined conversion)
STATIC_CHECK(iris::is_assignable_without_narrowing_v<std::string&, const char*>);
STATIC_CHECK(iris::is_assignable_without_narrowing_v<convertible_from_int&, int>);

// Not assignable at all
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<not_convertible_from_int&, int>);
STATIC_CHECK(!iris::is_assignable_without_narrowing_v<int&, std::string>);
}


TEST_CASE("specialization_of")
{
STATIC_CHECK(iris::is_ttp_specialization_of_v<tuple<>, tuple>);
Expand Down
Loading