On 09/09/2026 12:38, Richard Biener wrote:
On Tue, Sep 8, 2026 at 11:15 PM feedable <[email protected]> wrote:

On 08/09/2026 11:34, Richard Biener wrote:
On Sat, Sep 5, 2026 at 7:39 PM feedable <[email protected]> wrote:
On 25/08/2026 13:48, Richard Biener wrote:
On Tue, Aug 25, 2026 at 1:26 AM feedable via Sourceware Forge
<[email protected]> wrote:
From: feedable <[email protected]>

Currently, auto_vec defaults its copy&move operations. This is bad,
since a user that tries to copy or move an auto_vec will unknowingly
set himself up for trouble, since the pointers from the base and members
will be copied verbatim, leading to double free situtaions and the like.
For auto_vec<T, N> with N != 0 we do not want these, instead they should
be deleted.  As seen in your implementation the complexity involved with
having to deal with the stack backing for small number of elements makes
this very inefficient.  If copy/move is needed people should use auto_vec<T, 0>
instead.
As stated in the discussion for v1, a move constructor is required for a
value to be returned. Values of type auto_vec<T, N> with N != 0 are
returned in the codebase already (yes, this is currently UB if the
constructor is not elided).
IMO those uses should be fixed.
I'm sorry but I still fail to understand the purpose of forbidding those
operations, given that an implementation of these operations is present
in this patch.

It is fairly clear to me that forbidding these operations will incur
costs on the codebase, such as denying storage of `auto_vec`s in
containers (which is being used in the following patch), and denying the
ability to return them (which is used now in the code elsewhere).

What is not clear to me is the benefits of such a decision.

Regarding the efficiency of my implementation of those ops, claims about
that can only be made in comparison, and the alternative here is to
unconditionally allocate on the heap, which is worse than storing the
data inline and eliding the move on return.
I'm not sure it is worse.

Until the net-benefits are clear to me, I cannot in good faith move
forward with this change.
The argument is that once you expose an inefficient API you'll
get lazy uses and inefficiencies that could have been easily avoided.
But I might be overly pessimistic here, not knowing how often the
copy/move construction/assignment ops for auto_vec would occur.

There is also the semantic difference with vec<> where
assignment does a shallow copy of the m_vec pointer and _not_
copy the data as with your non-move assignment/ctor.  So can
we at least avoid that?  vec<> has vec<>::copy for this and
truncate() + splice() for the assignment.

We should possibly delete vec<>::operator=() and adjust code
to use const vec<>& argument passing (it's one more indirection
sadly).  Adding move assignment might be useful, but I'd like
to explicitly "see" it, like if we can force v1 = std::move (v2); or so?

Richard.
I think there's a bit of a misunderstanding going on here, so allow me
to try to elaborate in a simplified way on semantics of initialization
and copy&move operations in C++.

In C++, in order to facilitate integration with its resource management
model, there are currently two kinds of references: lvalue (T &) and
rvalue (T &&).

lvalue references, as the name implies, usually bind to long-lived
values, such as values referenced by a name.
rvalue references, on the contrary, usually bind to short-lived values,
such as expression temporaries.

For example,
```cpp
struct s {};
s get_s();

void use_s(s &); /* (1) */
void use_s(s &&); /* (2) */

void test1() {
  s val;

  /* Here, `val` is a long-lived value, so (1) is picked */
  use_s(val);
  /* Here, the result of `get_s()` is a temporary value, so (2)
     is picked */
  use_s(get_s());
}
```
Now, this choice of function based on the kind of expression is a
property of overload resolution in general, so it works on ordinary
functions, as well as member functions, operator overloads,
constructors, etc.

To illustrate,
```cpp
struct t {
  t() {}
  t (t &) {} /* (1) */
  t (t &&) {} /* (2) */
};

t get_t();

void test2() {
  t x;
  t y{x}; /* For the same reasons as above, (1) is picked */
  t z{get_t()}; /* For the same reasons as above, (2) is picked */
}
```

This distinction is useful, since if we know that the value passed is a
temporary, we may avoid copying everything that existed in the previous
value, and just get the resource handles from the old value and leave
the value empty.

For example,
```cpp
#include<vector>

std::vector<int> yield_values();

void test3() {
  /* The return value of `yield_values()` is a temporary, so there is no
     problem in moving the memory handle from the temporary to the value
     initialized, and leaving the temporary empty, since it's not going
     to be used anymore. */
  std::vector<int> values = yield_values();
}
```

It's important to note that leaving the temporary empty is necessary,
since the destructor for that value will still be executed at some
point, and that destructor needs to be aware of the fact that the
resource the value managed is now managed elsewhere.

There are situations, however, where a value is persistent, but we know
it's not going to be used in the future, so we want to force the
initialization/assignment to be of the moving variety:
```cpp
std::vector<int> get_data();
bool act_on_summary(std::vector<int> &);

void test4() {
  std::vector<int> summary;
  for(;;) {
    std::vector<int> temporary = get_data();
    if (act_on_data(temporary)) {
      summary = std::move(temporary); /* `std::move` binds an rvalue
      reference to `temporary`, nothing more. It is equivalent to
      `(std::vector<int> &&)temporary` */
    }
  }
}
```

But as we can see, `std::move` is merely a marker of intent, where the
compiler does not automatically pick the right operation for the
context.


The default language infrastructure also automatically derives the
definitions of copy and move operations for aggregates:
```cpp
struct composite {
  std::vector<int> as;
  std::vector<int> bs;
};

composite get_composite();

void test5() {
  /* Now, there isn't a single resource handle we can obtain, so
     compiler-generated move operation performs move construction on
     its members in order: `as` first, and `bs` second. */
  composite data = get_composite();
}
```

This derivation is only available for copy and move constructors and
assignment operators.

Now, `auto_vec` is a hybrid of these concepts, as its storage itself may
be inline (modeled by the member case) or out-of-line (modeled by the
std::vector case). Other instances of this situation include
`std::string` and `std::function`, both of which can store the objects
they own inline or out-of-line, and their implementations of the
copy&move operations are similar to the one proposed in this patch, and
it's easy to see that it is as efficient as such an operation can
possibly be.

As for `vec`, this type by itself is not responsible for managing its
content, it has a trivial destructor, so for all intents and purposes it
behaves like a non-owning pointer to the data, so deleting copy and move
operations there is pointless.

One more thing to note: the compiler actually may (until C++17) or
must (since C++17) elide move operations where the value never ends up
being named, and may elide move operations where the value does end up
being named:
```cpp
std::vector<int> test6() {
  return {2, 3, 4};
}
std::vector<int> test7() {
  std::vector<int> res{2, 3, 4};
  return res;
}

void test8() {
  std::vector<int> res1 = test6(); /* Can (until C++17) or must
    (since C++17) be a direct initialization without any copy or move
    operation. */
  std::vector<int> res2 = test7(); /* Can be a direct initialization
    without any copy or move operation. */
}
```
In the latter case, direct initialization means that `test7`'s return
variable, `res`, will denote the same object as `test8`'s `res2`.

This will only occur, though, if it is perfectly and entirely clear to
the compiler that those objects should be one and the same. In
particular, having e.g. `std::move` here, while it makes the move very
visible, also prevents elision and thus pessimizes the code.

It is important to emphasize that these mechanics are an integral part
of the language, and not using them will only add churn to the user's
code, since operations will no longer be derived for aggregates, and
containers will have to special-case types which have special patterns
for copying.

Even GCC itself uses this infrastructure in e.g. `hash_table` and
`hash_map`, which already have copy-constructors, which in turn require
the contained types to be copyable (via standard language means)
as well.

From my knowledge, those types only work with `vec` semi-properly, and
that is only because those are non-owning pointers and have copy&move
operations defaulted. Still, if a `hash_map<vec<T>>` is global and not
cleaned in time, its global destructor will not clean up resources that
the `vec`s reference, producing leaks. Copies also do not copy the
contents of `vec`s, so we end up with aliased maps, and if one contained
`vec` is resized, the corresponding one in the copy silently becomes
invalid, causing use-after-frees.

The solution there is to use `auto_vec`s, since `hash_map`s expect to
retain ownership over their values, but `auto_vec`s need proper copy
and move operations defined for `hash_map`s to be able to use them.

Very similar situation occurs with `vec<vec<T>>`, where even a manual
copy produces aliased unusable results, with a similar solution of using
`auto_vec<auto_vec<T>>`, irrespective of whether there is an inline
buffer or not.

I hope it clears up the misunderstandings and allows us to progress in
this discussion.

Thanks,
feedable

Reply via email to