Can one forward declare a function taking a vector of incomplete type with a default value?

Can one forward declare a function taking a vector of incomplete type with a default value?


6

Below code snippet demonstrates some real issue I faced recently in my program:

#include<vector>

class A;

void f( const std::vector<A> & = {} );

There is an incomplete class A, and a function declaration taking a vector of A‘s with empty default value. And the function is not even called anywhere.

It works fine in GCC, and in Clang 14, but starting from Clang 15 an error appears:

In file included from <source>:1:
/opt/compiler-explorer/clang-15.0.0/bin/../include/c++/v1/vector:540:52: error: arithmetic on a pointer to an incomplete type 'A'
        {return static_cast<size_type>(__end_cap() - this->__begin_);}
                                       ~~~~~~~~~~~ ^
/opt/compiler-explorer/clang-15.0.0/bin/../include/c++/v1/vector:760:56: note: in instantiation of member function 'std::vector<A>::capacity' requested here
      __annotate_contiguous_container(data(), data() + capacity(),
                                                       ^
/opt/compiler-explorer/clang-15.0.0/bin/../include/c++/v1/vector:431:7: note: in instantiation of member function 'std::vector<A>::__annotate_delete' requested here
      __annotate_delete();
      ^
<source>:5:32: note: in instantiation of member function 'std::vector<A>::~vector' requested here
void f( const std::vector<A> & = {} );
                               ^
<source>:3:7: note: forward declaration of 'A'
class A;
      ^

Online demo: https://godbolt.org/z/a8xzshbzP

Are newer versions of Clang correct in rejecting the program?

4 Answers
4


4

Yes, Clang is correct to reject the program. Per vector.overview#4:

An incomplete type T may be used when instantiating vector if the allocator meets the allocator completeness requirements. T shall be complete before any member of the resulting specialization of vector is referenced.

In the default argument of f you’re referencing a constructor of vector<A> before A is complete, so the program is ill-formed.

Here’s a bug report (closed as invalid) showing a similar situation. The comment at the bottom suggests why this may have changed in Clang-15.

Probably what changed between libc++14 and libc++15 is that the vector move constructor became constexpr, so it’s now getting instantiated earlier.


1

This behavior is new to me, but moving forward, it is still possible to have a default parameter by redeclaring the function once the class is complete:

#include<vector>

class A;

void f( const std::vector<A> & );

class A{};

void f( const std::vector<A> & = {});

void f( const std::vector<A> & a ) {(void)a;}

int main() {
    f();
}

https://godbolt.org/z/vMxWhW18v


0

TL;DR: The declaration is invalid in C++20 and later. This is a consequence of constexpr vector.

  1. A constexpr member function of a templated class is implicitly instantiated if it is used in a potentially-evaluated expression.
    1. A member of a templated class is implicitly instantiated if the existence of the definition of the member affects the semantics of the program. ([temp.inst]/4)
    2. The existence of a function definition is considered to affect the semantics of the program if the function is needed for constant evaluation (even if constant evaluation of the expression is not required). ([temp.inst]/8)
    3. A function is needed for constant evaluation if it is a constexpr function that is named by an expression that is potentially constant evaluated. ([expr.const]/15)
    4. A function is named by an expression if it is selected in an overload resolution performed as part of forming that expression, unless it is a pure virtual function. ([basic.def.odr]/3)
    5. An expression is potentially constant evaluated if it is a potentially-evaluated expression. ([expr.const]/15)
  2. In C++20, vector‘s destructor is a constexpr member function. ([vector.overview])
  3. The destructor requires the element type to be complete. ([vector.overview]/4)

Therefore, in C++20, the default argument causes the destructor to be instantiated, which demands a complete element type.

Before C++20, the destructor is not instantiated, and thus the declaration is valid.

1

  • I don't think this is correct. Member functions being constexpr might be the reason why the OP's example gets diagnosed, but it's always been incorrect, even in previous revisions. I'm not entirely sure if the example is ill-formed or UB, but it's definitely not valid.

    – cigien

    10 hours ago



0

This is an untested hypothetical solution with possibly many UBs and pitfalls. But I see a case for some PIMPL pattern here:

Given that allegedly every member of std::vector delegates its element manipulation functionality to the allocator of the vector, if you provide a costume allocator – with all allocator functions declared – compilation succeeds iff the implementation of allocator method reside in the same TU that implementd the element type:

class element;
struct pimpl_allocator
: std::allocator<element>{
// redeclare every single method in std::allocator 
};

std::vector<element, pimpl_allocator> pimpl_vector;

If the members of pimpl_allocator are defined in the same CPP file containing full declaration of element, the program hopefully compiles.



Leave a Reply

Your email address will not be published. Required fields are marked *