1

I'm poking around in the myst of C++ instantiation / declaration order. Here's a fun bit I came across:

This compiles :

#include <cstddef>
#include <variant>
#include <array>

template <size_t V>
struct container
{
    // THIS COMPILES
    struct array;
    using val = std::variant<std::monostate, int, array>;

    // THIS DOESNT
    // using val = std::variant<std::monostate, int, struct array>;

    struct proxy : val
    {
        using val::variant;
    };

    struct array { };
};

int main()
{
    container<10> ctr;
}

But when you opt for in-place declarations, it suddenly stops working (Demo):

#include <cstddef>
#include <variant>
#include <array>

template <size_t V>
struct container
{
    // THIS COMPILES
    // struct array;
    // using val = std::variant<std::monostate, int, array>;

    // THIS DOESNT
    using val = std::variant<std::monostate, int, struct array>;

    struct proxy : val
    {
        using val::variant;
    };

    struct array { };
};

int main()
{

    container<10> ctr;
}

This is the error I get:

/opt/compiler-explorer/gcc-trunk-20220729/include/c++/13.0.0/type_traits:1012:52: error: static assertion failed: template argument must be a complete class or an unbounded array
 1012 |       static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}),
      | 

Can someone explain me exactly why this happens? What is the difference?

EDIT: You are allowed in certain circumstances to declare a type in a template argument list:

#include <cstddef>
#include <variant>
#include <array>
#include <cstdio>

void foo(std::initializer_list<struct array>);

struct array
{
    array(int a) : a_{a} {}
    void print() {
        printf("%d\n", a_);
    }
    int a_;
};

void foo(std::initializer_list<struct array> init) {
    for (auto a : init) {
        a.print();
    }
    printf(".. it works\n");
}

int main()
{
    foo({1,2,3});
}

I don't know when and where this applies though.

glades
  • 3,778
  • 1
  • 12
  • 34
  • 2
    Pretty sure your not allowed to declare a type in a template parameter list – NathanOliver Jul 29 '22 at 13:09
  • *"There are rules for when we can/cannot use `struct S1` as a replacement for just `S1`. And assuming that we can always use `struct S1` as a replacement for `S1` is wrong."* See [dupe](https://stackoverflow.com/a/73034955/12002570) – Jason Jul 29 '22 at 13:11
  • @NathanOliver Yes you can under certain circumstances check my updated question – glades Jul 29 '22 at 13:22
  • @glades Can you also add the error that you get when you use `struct array`. That will make your question more/much better. – Jason Jul 29 '22 at 13:25
  • 1
    @AnoopRana Yes, added. Could you reopen the question? I understand the link but its really not the same.. – glades Jul 29 '22 at 13:29
  • @NathanOliver I don't think there is anything prohibiting an elaborated type specifier in a template argument and the elaborated type specifiers can always declare new classes. But confusions such as this are why I think it is a bad idea to use elaborated type specifiers outside of dedicated forward-declarations. – user17732522 Jul 30 '22 at 00:38
  • Your title does not match your question well. I was expecting to see the first code block contrasted with what you would get if you removed `template struct container {` and the closing brace. That is, a direct comparison of "in class context" to "not in class context". (The fact that you are using a class template instead of a class is also inconsistent with your title -- do you really need to complicate things with templates? Would a non-template class definition demonstrate things?) – JaMiT Jul 30 '22 at 01:49
  • *"// THIS COMPILES"*. Not sure if it is not already ill-formed. `array` is also incomplete at this point. – Jarod42 Aug 01 '22 at 08:48
  • @Jarod42 The `using` declaration itself does not cause implicit instantiation of the template specialization, so it doesn't matter whether `array` is incomplete at that point. Because `container` is a template, definitions for the individual member classes will also only be instantiated when required. So actually nothing ever instantiates the `std::variant` in the shown code. – user17732522 Aug 01 '22 at 14:00
  • The non-working version is however IFNDR, because `val` is not dependent on a template parameter and non-dependent constructs may not cause a hypothetical instantiation immediately following the template definition to be ill-formed. If you add a dependent type to `val` the code will be well-formed. The working version is fine because `array` is a member of the template specialization and therefore makes the `variant`'s type dependent so that this IFNDR rule doesn't apply anymore. – user17732522 Aug 01 '22 at 14:00

1 Answers1

3

A declaration of the form

class-key attribute-specifier-seq(opt) identifier;

(where class-key means struct, class, or union), declares the identifier in the scope where the declaration appears ([dcl.type.elab]/2). That means in your first code snippet, array is forward declared as a member class of the container class template and you can later define it within the same scope.

When an elaborated-type-specifier such as struct array appears as a component of some larger declaration or expression, [dcl.type.elab]/3 applies. First, the compiler looks up the name array, ignoring anything that is not a type ([basic.lookup.elab]/1). If it doesn't find anything, then it forward-declares array in "the nearest enclosing namespace or block scope". Consequently, the struct array that you later define as a nested class is a different class, and the one you referred to as struct array earlier has no definition.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • 1
    I can't see where [basic.lookup.elab/1](http://eel.is/c++draft/basic.lookup.elab#1) says that it looks for the idenifier in the "the nearest enclosing namespace or block scope" which is **not** the `container`'s class scope in this case. Could you please elaborate? (if the nearest enclosing scope is not the class scope, what is it here?) – The Dreams Wind Jul 30 '22 at 08:29
  • 2
    @TheDreamsWind It is not the lookup that is done in the nearest enclosing namespace or block scope. That is just the target scope of the declaration that is introduced by the elaborated type specifier if lookup fails. It is in basic.lookup.elab/3. A class scope is not a block or namespace scope, so the nearest such scope is the global scope. The elaborated type specifier is declaring `::array`. – user17732522 Jul 30 '22 at 13:21
  • TL;DR: Elaborated type specifiers are confusing and best avoided, except in a declaration by themselves where they follow more sensible rules and can be informally treated as *class-specifier*s without a body. – Davis Herring Jul 30 '22 at 15:30
  • @user17732522 So.. `struct something` declares into the nearest block / namespace scope (note: not class scope) if the refered-to struct is not found according to normal lookup rules? Yes, this is confusing, because you would bet that it is introduced into the same scope that lookup failed for.. – glades Aug 01 '22 at 13:16
  • @user17732522 Correct me if I'm wrong but the concept of forward declaring this way was introduced in C++. Just the lookup-part would have sufficed for C compatibility, no? – glades Aug 01 '22 at 13:38
  • @glades No, you can do the same in C, although the rules about the target scope of the declaration are a bit different (and obviously there are no template arguments). – user17732522 Aug 01 '22 at 13:52