c++g++language-lawyerclang++

`static_cast<const bool&>` with `explicit operator bool`


Consider the following:


struct C {
    explicit operator bool() const {
        return true;
    }
};

int main() {

    C c;

    auto b = static_cast<const bool &>(c);

    return 0;

}

Clang++ 18 compiles fine, G++ 14 says:

test.cpp: In function ‘int main()’:
test.cpp:12:14: error: invalid ‘static_cast’ from type ‘C’ to type ‘const bool&’
   12 |     auto b = static_cast<const bool &>(c);
      |              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

See on Compiler Explorer.

This incompatibility arose in my code because Catch2 test macros use that kind of static_cast under the hood and it fails with a custom type with an explicit operator bool, but only with G++.

It seems to me that since a bool temporary can bind to a const bool &, this should be allowed.

Who's right here, following the standard?


Solution

  • TL;DR

    Clang is right.

    Details:

    When you type operator bool()explicit the complete restriction that explicit causes is described here:

    [class.conv.fct]/2

    A conversion function may be explicit ([dcl.fct.spec]), in which case it is only considered as a user-defined conversion for direct-initialization ([dcl.init]). Otherwise, user-defined conversions are not restricted to use in assignments and initializations.

    static_cast does not involve assignments, just initializations. Are the initializations that occur in a static_cast direct or not?

    [dcl.init.general]/15:

    The initialization that occurs

    • (15.1) for an initializer that is a parenthesized expression-list or a braced-init-list,
    • (15.2) for a new-initializer ([expr.new]),
    • (15.3) in a static_cast expression ([expr.static.cast]),
    • (15.4) in a functional notation type conversion ([expr.type.conv]), and
    • (15.5) in the braced-init-list form of a condition

    is called direct-initialization.

    the standard explicitly states that initialization that occurs in a static_cast expression is a direct-initialization.

    So explicit cannot restrict how operator bool() works in a static_cast. (ok, it could restrict it if we ended up having a concept restriction that queried assignment properties of the right hand side type with being assigned to a bool, but I'm not even sure HOW I'd write that!)

    What is going on when you static cast to a reference?

    [expr.static.cast]/4:

    If T is a reference type, the effect is the same as performing the declaration and initialization T t(E); for some invented temporary variable t ([dcl.init]) and then using the temporary variable as the result of the conversion.

    This initialization, by [dcl.init.general]/15, is a direct-initialization, so operator bool()const explicit is permitted to be used.

    I don't think it is in doubt that

    struct foo {
      operator bool()const{return true;}
    };
    foo f;
    bool const& b = f;
    

    is perfectly legal; so, clang is right, the other compilers are in error.

    ...

    Apparently there is doubt that bool const& b = f; is legal when f has a non-explicit operator bool()const. So here is my evidence it is legal:

    bool const& b = f; is legal due to [dcl.init]/5.1.2 which permits binding to a const-lvalue-ref-to-T from an object that can be converted to an rvalue-to-T.

    bool const& b = f; is safe to due "temporary materialization" and reference lifetime extension causing the materialized temporary to last as long as the const-lvalue-ref-to-T does.

    Note that the use of = here would not be legal if the operator bool was explicit, it would have to be bool const& b(f); if f has an explicit operator bool; however, this section merely exists to point out that without explicit there is no problem. Prior logic shows that explicit does not cause a problem, as the initialization in a static_cast is, well, an explicit one.