4

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;
      |             ^~~~
wohlstad
  • 12,661
  • 10
  • 26
  • 39
glades
  • 3,778
  • 1
  • 12
  • 34

2 Answers2

6

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).

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 1
    Interesting I didn't think of instantiation happening regardless of choosen path, but that would explain it. Afaik clang has the same behaviour. As for std::same_as you can just use it that way. It's a concept and not a type trait (although based on the type trait you mention). – glades Jan 25 '23 at 08:08
  • @glades Ahh, facepalm. Edited. – HolyBlackCat Jan 25 '23 at 08:13
  • 1
    One potential reason for why the rhs needs to be instanciated could be that `operator&&` could be overloaded - and the compiler needs to check if such an overload exists, and for that both the type of lhs and the type of rhs must be known. – Turtlefight Feb 08 '23 at 17:40
3

You can force the compiler to evaluate the 2nd condition only if cnt > 0 using if constexpr (available since c++17) and separating into 2 nested ifs:

#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);

//-----vvvvvvvvv---------
    if constexpr (cnt > 0)
    {
        if (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();
}

Output:

last is int
wohlstad
  • 12,661
  • 10
  • 26
  • 39
  • It does work with two nested if constexpr() but why doesn't it work with && operator? – glades Jan 25 '23 at 07:07
  • I am not sure (Upvoted the qeustion because I think it is a good one). But I thought you might find my answer useful since it does offer a way to achieve what you need. – wohlstad Jan 25 '23 at 07:10
  • Thanks yes I'm going for this of course. I was just curious :) – glades Jan 25 '23 at 07:13