Why does my (C++) compiler want to instantiate my parameter pack class when using std::endl?
Asked Answered
D

1

19

Consider the following short program.

#include <iostream>

template< typename ... Ts >
class foobar
{
    static_assert( sizeof...(Ts) > 0 );
};

template< typename ... Ts >
std::ostream& operator<<( std::ostream& o, const foobar< Ts... >& )
{
    return o;
}

int main(void)
{
    std::cout << "This has nothing to do with foobar" << std::endl;
}

When we try to compile this, we get...

ted@tedscomputer:~/Projects/emptypack$ g++ -o emptypack main.cpp
main.cpp: In instantiation of ‘class foobar<>’:
main.cpp:17:63:   required from here
main.cpp:6:34: error: static assertion failed
    6 |     static_assert( sizeof...(Ts) > 0 );
      |                    ~~~~~~~~~~~~~~^~~
ted@tedscomputer:~/Projects/emptypack$

I understand that std::endl is a bit of an odd bird (so much so that it actually has its own StackOverflow tag), but what exactly is going wrong here? Why is the compiler trying to instantiate my completely unrelated class with empty type parameters, failing, and then generating an error?

Maybe more to the point, every time I write a class that takes a parameter pack, do I have to make sure it can be instantiated with no type parameters, even if that doesn't make any sense w.r.t. the program logic, just in case it comes within a mile of std::endl?

Derwent answered 10/5, 2022 at 14:16 Comment(3)
For anyone wondering, this happens on both GCC, Clang, and MSVC, and all three provide similar useless diagnostics.Hsiuhsu
Somehow moving the condition to a requires fixes the issue.Hsiuhsu
@Hsiuhsu Yes because that would make the deducing Ts as an empty parameter pack (https://mcmap.net/q/512196/-when-are-template-parameter-packs-deduced-as-empty/5754656) a substitution failure instead of hard failing when instantiating the class (to look for a converting constructor I think?)Boydboyden
H
7

@Artyer found this:

[temp.arg.explicit]/4

... [Note 1: A trailing template parameter pack ... not otherwise deduced will be deduced as an empty sequence of template arguments. — end note]

Normally, if you just pass an object of a specific type to the second argument, the condition is not met:

::operator<<(std::cout, foobar<int>{}); // compiles

Even if you pass an object of a different type, the condition is still not met:

// Causes `error: no matching function for call to 'operator<<'`,
// does NOT trigger the `static_assert`.
::operator<<(std::cout, 0);

I assume this is because the compiler does attempt to deduce Ts... from the argument type and fails.

And if we spell the argument as std::enable_if_t<true, const foobar<Ts...> &>, the condition is always met, because it's impossible deduce Ts... from it, and the compiler doesn't even try.

But why is the condition met in our case? Because std::endl, being a function template (without template args specified) doesn't have a type, so it's impossible to deduce anything from it.

Hsiuhsu answered 10/5, 2022 at 16:54 Comment(1)
Some high profile guy mocked me for using endl like this: endl(std::cout<<"whatever the hell that comes next"); This looks quite expressive, simple and less prone to such failures.Simulant

© 2022 - 2024 — McMap. All rights reserved.