3
\$\begingroup\$

Usually in C++, the language rules automatically convert types as necessary. For example, I can pass a pointer-to-derived in place of a pointer-to-base, and the same with references.

There are occasions where one needs to specify a type to which a value should be coerced, but less strongly than a cast. Some cases where we might need to do this include

  • two-step conversion, where we need to coerce to an intermediate type,
  • providing consistent arguments for function template type deduction,
  • adding const and/or volatile (but not casting them away),
  • ensuring that both alternatives of ?: are compatible.

My first attempt at this was An up-cast for C++, but there were serious problems with that. I've improved the motivational context, ensured that narrowing of primitive types and use of explicit conversions are prevented, and added tests.

That was pretty simple:

#include <utility>

namespace coercion
{
    template<typename From, typename To>
    concept coercible = requires(From&& value, void (*f)(To)) {
        f({std::forward<From>(value)});
        f(std::forward<From>(value)); // prevents base&& → derived&&
    };

    template<typename To, coercible<To> From>
    constexpr To coerce_to(From&& value)
    {
        return {std::forward<From>(value)};
    }
}

However, a review of the first attempt said:

Having to remember the right cv-qualification and adding a reference is a bit annoying. I'd just want to write up_cast<Base>(foo) and have it return a const Base& or Base& or maybe even a volatile Base*, depending on what foo is.

It's not possible to automatically reference-qualify the result type without conflicting with use for widening value types, but I was able to add the relevant cv-qualification with this more complex version:

#include <type_traits>
#include <utility>

namespace coerce
{
    // copy qualifications across
    template<typename From, typename To>
    using copy_const =
        std::conditional_t<std::is_const_v<From>,
                           std::add_const_t<To>,
                           To>;

    template<typename From, typename To>
    using copy_volatile =
        std::conditional_t<std::is_volatile_v<From>,
                           std::add_volatile_t<To>,
                           To>;

    template<typename From, typename To>
    using copy_cv = copy_const<From, copy_volatile<From, To>>;


    template<typename From, typename To>
    struct result { using type = To; };

    template<typename From, typename To>
    struct result<From*,To*> {
        using type = std::add_pointer_t<
            copy_cv<From,
                    typename result<std::remove_cv_t<From>, To>::type>>;
    };

    template<typename From, typename To>
    struct result<From&,To&> {
        using type = std::add_lvalue_reference_t<
            copy_cv<From,
                    typename result<From, To>::type>>;
    };

    template<typename From, typename To>
    struct result<From&&,To&&> {
        using type = std::add_rvalue_reference_t<
            copy_cv<From,
                    typename result<From, To>::type>>;
    };



    template<typename From, typename To>
    using result_t = result<From, To>::type;

    template<typename From, typename To>
    concept coercible = requires(From&& value, void (*f)(result_t<From,To>)) {
        f({std::forward<From>(value)});
        f(std::forward<From>(value));
    };

    template<typename To, coercible<To> From>
    constexpr result_t<From,To> to(From&& value)
    {
        return {std::forward<From>(value)};
    }
}

The compile-time test suite is quite substantial:

#include <cstdint>

// Using concepts for these tests improves error diagnostics
template<typename From, typename To>
concept can_coerce =
    requires(From f) { coerce::to<To>(std::forward<From>(f)); };

template<typename From, typename To, typename Expected>
concept coerce_result_fails = std::is_same_v<Expected, void> && !can_coerce<From, To>;

template<typename From, typename To, typename Expected>
concept coerce_result_passes = coerce::coercible<From, To>
    && std::same_as<coerce::result_t<From, To>, Expected>;

template<typename From, typename To, typename Expected>
concept coerce_result_is =
    coerce_result_fails<From, To, Expected> || coerce_result_passes<From, To, Expected>;


// widening (or preserving) integer conversions
static_assert(coerce_result_is<int, int, int>);
static_assert(coerce_result_is<bool, bool, bool>);
static_assert(coerce_result_is<bool, char, char>);
static_assert(coerce_result_is<std::uint8_t, std::uint16_t, std::uint16_t>);
static_assert(coerce_result_is<const std::uint8_t&, std::uint16_t, std::uint16_t>);
static_assert(coerce_result_is<std::uint16_t, std::int32_t, std::int32_t>); // safe sign change

// no narrowing integer conversions
static_assert(coerce_result_is<char, bool, void>);
static_assert(coerce_result_is<std::uint32_t, std::int16_t, void>); // smaller range
static_assert(coerce_result_is<std::int16_t, std::uint32_t, void>); // unsafe sign change


// pointers - can add cv-qualification
static_assert(coerce_result_is<int*, int*, int*>);
static_assert(coerce_result_is<int*, const int*, const int*>);
static_assert(coerce_result_is<int*, volatile int*, volatile int*>);
static_assert(coerce_result_is<int*, const volatile int*, const volatile int*>);
static_assert(coerce_result_is<const int*, const volatile int*, const volatile int*>);
static_assert(coerce_result_is<volatile int*, const volatile int*, const volatile int*>);
// existing cv-qualification is retained
static_assert(coerce_result_is<const int*, int*, const int*>);
static_assert(coerce_result_is<volatile int*, int*, volatile int*>);
static_assert(coerce_result_is<const volatile int*, int*, const volatile int*>);
static_assert(coerce_result_is<const volatile int*, const int*, const volatile int*>);
static_assert(coerce_result_is<const volatile int*, volatile int*, const volatile int*>);

// lvalue references
static_assert(coerce_result_is<int&, const int&, const int&>);
static_assert(coerce_result_is<int&, volatile int&, volatile int&>);
static_assert(coerce_result_is<int&, const volatile int&, const volatile int&>);
static_assert(coerce_result_is<const int&, const volatile int&, const volatile int&>);
static_assert(coerce_result_is<volatile int&, const volatile int&, const volatile int&>);
// existing cv-qualification is retained
static_assert(coerce_result_is<const int&, int&, const int&>);
static_assert(coerce_result_is<volatile int&, int&, volatile int&>);
static_assert(coerce_result_is<const volatile int&, int&, const volatile int&>);
static_assert(coerce_result_is<const volatile int&, const int&, const volatile int&>);
static_assert(coerce_result_is<const volatile int&, volatile int&, const volatile int&>);

// rvalue references
static_assert(coerce_result_is<int&&, const int&&, const int&&>);
static_assert(coerce_result_is<int&&, volatile int&&, volatile int&&>);
static_assert(coerce_result_is<int&&, const volatile int&&, const volatile int&&>);
static_assert(coerce_result_is<const int&&, const volatile int&&, const volatile int&&>);
static_assert(coerce_result_is<volatile int&&, const volatile int&&, const volatile int&&>);
// existing cv-qualification is retained
static_assert(coerce_result_is<const int&&, int&&, const int&&>);
static_assert(coerce_result_is<volatile int&&, int&&, volatile int&&>);
static_assert(coerce_result_is<const volatile int&&, int&&, const volatile int&&>);
static_assert(coerce_result_is<const volatile int&&, const int&&, const volatile int&&>);
static_assert(coerce_result_is<const volatile int&&, volatile int&&, const volatile int&&>);

// references to pointers
static_assert(coerce_result_is<int*&, int*&, int*&>);
static_assert(coerce_result_is<int*&&, int*&&, int*&&>);
static_assert(coerce_result_is<int*&, const int*&, void>);
static_assert(coerce_result_is<int*&, volatile int*&, void>);
static_assert(coerce_result_is<int*&&, const int*&&, void>);
static_assert(coerce_result_is<int*&&, volatile int*&&, void>);

// no lvalue to rvalue conversions
static_assert(coerce_result_is<int&, int&&, void>);

// no prvalue to lvalue conversions
static_assert(coerce_result_is<int, int&, void>);
// but lifetime-extending is okay
static_assert(coerce_result_is<int, const int&, const int&>);
// and so is value to rvalue-ref
static_assert(coerce_result_is<int, int&&, int&&>);


// up-casts and down-casts

struct Base {
    // Prevent accidental object slicing to this base
    Base(auto const&) = delete;
    explicit Base(std::derived_from<Base> auto const& other)
        : Base{coerce::to<Base&>(other)} {}
};
struct Derived : Base {};

static_assert(coerce_result_is<Base, Base, Base>);
static_assert(coerce_result_is<Base, Derived, void>);
static_assert(coerce_result_is<Derived, Derived, Derived>);
static_assert(coerce_result_is<Derived, Base, void>); // slicing constructor is explicit

static_assert(coerce_result_is<Derived*, Base*, Base*>);
static_assert(coerce_result_is<Derived*, const Base*, const Base*>);
static_assert(coerce_result_is<const Derived*, Base*, const Base*>);
static_assert(coerce_result_is<volatile Derived*, Base*, volatile Base*>);
static_assert(coerce_result_is<const Derived*, volatile Base*, const volatile Base*>);
static_assert(coerce_result_is<volatile Derived*, const Base*, const volatile Base*>);

static_assert(coerce_result_is<Derived&, Base&, Base&>);
static_assert(coerce_result_is<const volatile Derived&, const volatile Base&, const volatile Base&>);

static_assert(coerce_result_is<Derived&&, Base&&, Base&&>);
static_assert(coerce_result_is<const Derived&&, Base&&, const Base&&>);
static_assert(coerce_result_is<volatile Derived&&, Base&&, volatile Base&&>);

// no down-casts
static_assert(coerce_result_is<Base*, Derived*, void>);
static_assert(coerce_result_is<Base&, Derived&, void>);
static_assert(coerce_result_is<Base&&, Derived&&, void>);

// no lvalue to rvalue conversions
static_assert(coerce_result_is<Base&, Base&&, void>);
static_assert(coerce_result_is<Derived&, Base&&, void>);

// no prvalue to lvalue conversions
static_assert(coerce_result_is<Derived, Base&, void>);
// but lifetime-extending is okay
static_assert(coerce_result_is<Derived, const Base&, const Base&>);
// and so is value to rvalue-ref
static_assert(coerce_result_is<Derived, Base&&, Base&&>);

I'm particularly interested in additional tests, especially if they expose weaknesses in the implementation.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ This looks very good. However, it fails to compile with Clang: missing typename in the declaration of result_t (easy fix), but more importantly the last two references-to-pointers asserts fail to compile. I don't know why though. \$\endgroup\$
    – G. Sliepen
    Jan 23 at 19:27
  • \$\begingroup\$ That's useful information. I used GCC 12, for the record. Not sure why it differs or which is more correct. \$\endgroup\$ Jan 23 at 20:37

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.