Skip to content

[cpp2util] as: add constexpr branch for any #1057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

sookach
Copy link
Contributor

@sookach sookach commented Apr 26, 2024

This patches #961. The issue lies in the fact that the as cast for any is defined at

cppfront/include/cpp2util.h

Lines 1625 to 1630 in 5663493

// as
//
template<typename T, typename X>
requires (!std::is_reference_v<T> && std::is_same_v<X,std::any> && !std::is_same_v<T,std::any>)
constexpr auto as( X const& x ) -> T
{ return std::any_cast<T>( x ); }

so the any call hits

cppfront/include/cpp2util.h

Lines 1351 to 1353 in 5663493

else {
return nonesuch;
}

even though the code provided in the issue is well within the specifications outlined at https://hsutter.github.io/cppfront/cpp2/expressions/

This pr adds another constexpr branch with the same requirements as the function (I think it is the simplest solution). Let me know if you have any suggestions. Thanks!

@sookach
Copy link
Contributor Author

sookach commented Apr 26, 2024

@wolfseifert

@sookach
Copy link
Contributor Author

sookach commented Apr 26, 2024

@filipsajdak tagging you because I'm not sure who the code owner is

@filipsajdak
Copy link
Contributor

I will take a look at this issue during the weekend.

@sookach
Copy link
Contributor Author

sookach commented Apr 26, 2024

Thanks!

@wolfseifert
Copy link

I can confirm that this PR resolves #961.

As a sole user (tester) of Cpp2 I cannot tell whether the proposed patch fits into cppfront's code design.

@hsutter
Copy link
Owner

hsutter commented May 8, 2024

Thanks for your pull request! It looks like this may be your first contribution to cppfront. I've sent the Contributor License Agreement (CLA) to your email, and once it's signed I can look at your pull request. Thanks again for your contribution.

@hsutter
Copy link
Owner

hsutter commented May 8, 2024

@filipsajdak I also want to make sure this doesn't conflict with your #1053?

@sookach
Copy link
Contributor Author

sookach commented May 8, 2024

Thanks for your pull request! It looks like this may be your first contribution to cppfront. I've sent the Contributor License Agreement (CLA) to your email, and once it's signed I can look at your pull request. Thanks again for your contribution.

Awesome! Done.

@hsutter
Copy link
Owner

hsutter commented May 12, 2024

Thanks! This PR would fix the problem, but it does it by repeating the as that already exists for std::any inside the general as. My aim was to demonstrate extensibility for user-defined is and as by having the standard library types' is and as be separate, and not built-in to the "language" in the language-support default implementations.

But it provides a clue to the problem: There is already an as for std::any later, which ought to be being called, but isn't.

We have these two functions, which differ mainly in the constness of the parameter:

template< typename C, typename X >
auto as(X const& x CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decltype(auto) {
    if constexpr (
        std::is_floating_point_v<C> &&
        std::is_floating_point_v<CPP2_TYPEOF(x)> &&
        sizeof(CPP2_TYPEOF(x)) > sizeof(C)
    )
    {
        return CPP2_COPY(nonesuch);
    }
    //  Signed/unsigned conversions to a not-smaller type are handled as a precondition,
    //  and trying to cast from a value that is in the half of the value space that isn't
    //  representable in the target type C is flagged as a type_safety contract violation
    else if constexpr (
        std::is_integral_v<C> &&
        std::is_integral_v<CPP2_TYPEOF(x)> &&
        std::is_signed_v<CPP2_TYPEOF(x)> != std::is_signed_v<C> &&
        sizeof(CPP2_TYPEOF(x)) <= sizeof(C)
    )
    {
        const C c = static_cast<C>(x);
        type_safety.enforce(   // precondition check: must be round-trippable => not lossy
            static_cast<CPP2_TYPEOF(x)>(c) == x && (c < C{}) == (x < CPP2_TYPEOF(x){}),
            "dynamic lossy narrowing conversion attempt detected" CPP2_SOURCE_LOCATION_ARG
        );
        return CPP2_COPY(c);
    }
    else if constexpr (std::is_same_v<C, std::string> && std::is_integral_v<X>) {
        return cpp2::to_string(x);
    }
    else if constexpr (std::is_same_v<C, X>) {
        return x;
    }
    else if constexpr (std::is_base_of_v<C, X>) {
        return static_cast<C const&>(x);
    }
    else if constexpr (std::is_base_of_v<X, C>) {
        return Dynamic_cast<C const&>(x);
    }
    else if constexpr (
        std::is_pointer_v<C>
        && std::is_pointer_v<X>
        && requires { requires std::is_base_of_v<deref_t<X>, deref_t<C>>; }
    )
    {
        return Dynamic_cast<C>(x);
    }
    else if constexpr (requires { C{x}; }) {
        //  Experiment: Recognize the nested `::value_type` pattern for some dynamic library types
        //  like std::optional, and try to prevent accidental narrowing conversions even when
        //  those types themselves don't defend against them
        if constexpr( requires { requires std::is_convertible_v<X, typename C::value_type>; } ) {
            if constexpr( is_narrowing_v<typename C::value_type, X>) {
                return nonesuch;
            }
        }
        return C{x};
    }
    else {
        return nonesuch;
    }
}

template< typename C, typename X >
auto as( X& x ) -> decltype(auto) {
    if constexpr (std::is_same_v<C, X>) {
        return x;
    }
    else if constexpr (std::is_base_of_v<C, X>) {
        return static_cast<C&>(x);
    }
    else if constexpr (std::is_base_of_v<X, C>) {
        return Dynamic_cast<C&>(x);
    }
    else {
        return as<C>(std::as_const(x));
    }
}

Questions I'm investigating... @JohelEGP and @filipsajdak I'd value your observations please:

  • These two function bodies are similar but different, even though their interfaces seem to mainly differ only in constness. Why does the second one have only a few of the cases of the first one? I think @JohelEGP most recently touched this in refactor(util): merge built-in is/as type overloads #956.

  • If I move just the second (shorter) function to after the as for std::any, the example in [BUG] no match for operator<< and const cpp2::nonesuch_ #961 works fine and calls the as for std::any. But shouldn't it already be doing that in its current source location, given that as<C>(std::as_const(x)) is a dependent call (or is the point of instantiation somewhere in between somehow)?

@sookach
Copy link
Contributor Author

sookach commented May 12, 2024

This PR would fix the problem, but it does it by repeating the as that already exists for std::any inside the general as.

100% agree. The change I made is not the nicest approach, and for something as critical to the language as casting and type information it makes sense to take the time to come up with as clean of a solution as possible.

My aim was to demonstrate extensibility for user-defined is and as by having the standard library types' is and as be separate, and not built-in to the "language" in the language-support default implementations.

Ah, thanks for the info. I think your reasoning and aim make complete sense and the modularity/extensibility is something I would definitely prefer as a contributor and especially as a user.

But it provides a clue to the problem: There is already an as for std::any later, which ought to be being called, but isn't.

Honestly, even if the code change is way off the mark and ends up being replaced, figuring out the source of the issue would make this PR worthwhile in my opinion.

Curious to see what the resolution ends up being since we might be able to kill two birds with one stone (#1062 follows a similar pattern) by fixing this bug.

@sookach
Copy link
Contributor Author

sookach commented May 14, 2024

I've been playing around a bit and found that removing

cppfront/include/cpp2util.h

Lines 1533 to 1547 in 3604626

template< typename C, typename X >
auto as( X& x ) -> decltype(auto) {
if constexpr (std::is_same_v<C, X>) {
return x;
}
else if constexpr (std::is_base_of_v<C, X>) {
return static_cast<C&>(x);
}
else if constexpr (std::is_base_of_v<X, C>) {
return Dynamic_cast<C&>(x);
}
else {
return as<C>(std::as_const(x));
}
}

fixes both this issue and #1061

@hsutter
Copy link
Owner

hsutter commented May 15, 2024

Ah, right... it seems that the second as( X& x ) for non-const lvalues is a better match and hides the later overload that's intended for any which takes const&:

template<typename T, typename X>
    requires (!std::is_reference_v<T> && std::is_same_v<X,std::any> && !std::is_same_v<T,std::any>)
constexpr auto as( X const& x ) -> T
    { return std::any_cast<T>( x ); }

Archaeological spelunking in the pointer/reference casts

Disclaimer: The following reconstruction may not be exactly right. This is mostly notes-in-progress for myself to remember how we got here.

Aug 2022

Originally I added the const and non-const overloads for the pointer/reference cases to make the casts return the correctly-constified type:

template< typename C, typename X >
    requires (std::is_base_of_v<X, C> && !std::is_same_v<C,X>)
auto as( X& x ) -> C& {
    return dynamic_cast<C&>(x);
}

template< typename C, typename X >
    requires (std::is_base_of_v<X, C> && !std::is_same_v<C,X>)
auto as( X const& x ) -> C const& {
    return dynamic_cast<C const&>(x);
}

template< typename C, typename X >
    requires (std::is_base_of_v<X, C> && !std::is_same_v<C,X>)
auto as( X* x ) -> C* {
    return dynamic_cast<C*>(x);
}

template< typename C, typename X >
    requires (std::is_base_of_v<X, C> && !std::is_same_v<C,X>)
auto as( X const* x ) -> C const* {
    return dynamic_cast<C const*>(x);
}

Jan 2024, PR 956

Then #956 refactored this, but ended up with two functions, pretty much as it is now that I pasted earlier above:

  • the one for const& that had all the pointer/reference casts but also more cases/logic (e.g., merged in signedness logic)
  • the one for & that only has reference cases (not pointer casts or the other)

So we want to finish #956's work in unifying these.

I think we don't want to just duplicate all the other logic from the const& overload in the & overload, because then those two will be consistent but the & overload will still hide customizations like the one for any.

I think instead we want to drop the & overload (as suggested above) and change the const& one to be a T&& forwarder and then do the correct const propagation in the cast sections of that function. That might work... it would make these two consistent (by unifying them) and shouldn't hide the customizations which will still be a better match I think (unless forwarders are even more greedy than I remember... I guess I'm about to find out).

That's a snapshot of in-progress thinking... now to actually try the latter , will report back...

@hsutter hsutter closed this in 4b1b255 May 15, 2024
@hsutter
Copy link
Owner

hsutter commented May 15, 2024

Thanks! Even though I didn't take this PR as written, it illuminated the problem and helped more quickly narrow down what the problem was. The problems this PR was targeting should now be fixed with the above commit, please check.

Again, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants