Search code examples
c++tuplesif-constexprand-operator

strange behaviour of operator && in constexpr context


The following code tries to make compile-time decisions based on the last argument passed in a parameter pack. It contains a comparison if the number of parameter-pack arguments is > 0 and then tries to get the last element of it. However, the constructed tuple is accessed at an invalid index which is supposedly bigger than the max tuple index (as the static_assert shows).
How is that possible if I do cnt-1?

Demo

#include <cstdio>
#include <concepts>
#include <utility>
#include <tuple>


template <typename... Args>
auto foo(Args&&... args)
{
    auto tuple = std::forward_as_tuple(std::forward<Args>(args)...);
    constexpr std::size_t cnt = sizeof...(Args);

    if constexpr (cnt > 0 && std::same_as<std::remove_cvref_t<std::tuple_element_t<cnt-1, decltype(tuple)>>, int>) {
        printf("last is int\n");
    } else {
        printf("last is not int\n");
    }
}

int main()
{
    foo(2);

    foo();
}

Error:

/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/tuple: In instantiation of 'struct std::tuple_element<18446744073709551615, std::tuple<> >':
<source>:13:25:   required from 'auto foo(Args&& ...) [with Args = {}]'
<source>:24:8:   required from here
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/tuple:1357:25: error: static assertion failed: tuple index must be in range
 1357 |       static_assert(__i < sizeof...(_Types), "tuple index must be in range");
      |                     ~~~~^~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/tuple:1357:25: note: the comparison reduces to '(18446744073709551615 < 0)'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/tuple:1359:13: error: no type named 'type' in 'struct std::_Nth_type<18446744073709551615>'
 1359 |       using type = typename _Nth_type<__i, _Types...>::type;
      |             ^~~~

Solution

  • Short-circuiting that stops the rhs from being evaluated (having its value computed at runtime) doesn't stop it from being instantiated (having template arguments substituted into templates, checking them for validity, all at compile-time).

    I don't see any particular reason why it couldn't work the way you expect, it just wasn't added to the language (yet).

    As @wohlstad said, if constexpr is the solution:

    if constexpr (cnt > 0)
    {
        if constexpr (std::same_as<std::remove_cvref_t<std::tuple_element_t<cnt - 1, decltype(tuple)>>, int>)
        {
            ...
    
    

    The first if must be constexpr, while the second only should (in your scenario).