Luckily, the Zig core team has recognized it is an issue and plan to address it before 1.0 :)
https://github.com/ziglang/zig/issues/1108
You get all the benefits of Zig being able to choose the function ABI, but if the optimization would have caused a bug, you'll get an immediate panic at the function entrypoint, instead of silently corrupted data.
There is also a another paradigm where parameters are marked as in, out or inout parameters as in D (?) and cpp2 which makes the intent clear
Apparently, it's not (anymore?):
https://github.com/ziglang/zig/issues/5973#issuecomment-1801...
Are you speaking from experience?
As a former googler, I can say with certainty that the guidelines for passing any non primitive is pointer or ref.
string_view might be the only exception I can think of.
An unnecessary memory allocation is much more of a performance hit than suboptimal calling convention.
But yeah, in a c++ codebase, good modern practices are often verbose and clunky.
IMO it's hard to beat pass by value considering both performance and cognitive load.
...but that requires the argument to be a type from a template D: so you'd have to write:
template<typename String = std::string>
void dosomething(String &&str)
...and that's not quite right either, since you'd want it to be either an rvalue reference or a const lvalue referencestd::string in the case you cited is only really relevant if you std::move() into it and you would otherwise incur a string copy. Yes, it's bigger than 16 bytes (24 bytes), but that pales in comparison to the alternative.
(Taking std::string&& would eliminate the possibility of misuse / accidental copies, but that pattern is generally discouraged by Google's style guide for various reasons.)
Also, just because you see a certain pattern an awful lot even at Google doesn't mean that it's best practice -- there are plenty of instances of protobufs being passed by value...
1. `foo(T)` indicates polymorphic over both `T::T(const T&)` and `T::T(T&&)`. This gives you benefits of both pass-by-move (using `std::move` as needed), or copy.
2. Usage of `foo(T&&)` signals code-smell or an anti-pattern as `foo(T)` should be used instead unless it is perfecting forwarding / universal reference `template <typename T> foo(T&&)`.
What about std::span? std::optional? etc.
You also usually want to pass smart pointers (unique_ptr, shared_ptr) by value to avoid breaking your ownership model, or pass by raw pointer or reference to the held data. The obvious case that comes to mind where you'd want to write const unique_ptr<T>& is when iterating over a vector<unique_ptr<T>>. Otherwise, pass ptr.get() for an optional param, or *ptr for a required param where you've already checked ptr != nullptr.
Wrapper types like optional, variant, StatusOr? Usually pass by reference, unless the pointed-to type is small (<= 8 bytes).
One common pattern where I see wide structs being passed by value is the use of option structs (https://abseil.io/tips/173) that are usually constructed via designated initializers. There is some marginal benefit to being able to std::move() out individual fields but it's not really a big deal either way, as said computation is usually only done once on program initialization.
If that same object is directly passed it's just on the stack. So it's most likely in the cache.
It's a tradeoff between reducing memory usage and reducing cache misses.