Is C++ passing by value by default considered a design flaw?

OPEN

Being new to the language and reading lots of books (like the Effective series) I feel like I am spending half the time learning about ways to pass by reference, ways to avoid the pitfalls when passing by reference (dangling pointers and so on), deleting copy constructors, and all the subtly devious ways in which something might actually be copied under the hood even when you didn't intend for it to happen.

So my question is is there a reason it was designed to work this way when most of the time you intend to pass by reference anyway? It seems that languages like C# indeed work the other way around, so I am wondering whether this is considered a design flaw in the programming community or if there's actually a good reason for it?

Posted Sep 7, 2023, 12:37 AM UTC

139
scielliht987•

C++ has plenty of flaws, but passing by value isn't one of them.

You typically know when you're juggling things you maybe shouldn't blindly copy, and proceed accordingly.

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-conventional

1
twajblyn•

I think it might be inherited from C but that is only speculation on my part. The advantage is that pass-by-value(actually, pass-by-copy) makes a copy of the original data so that the original data is not modified. It also allows the data to be more complex than just a variable(like expressions, etc). Disadvantages are mainly that as the size of the data increases so does the overhead of copying the value. And since you are modifying a copy of the value you cannot detect that from the caller side. Here is a link if you want to read more https://icarus.cs.weber.edu/~dab/cs1410/textbook/6.Functions/value.html#:~:text=One%20of%20the%20advantages%20of,expression%20%2D%20not%20just%20a%20variable.

1
wrosecrans•

Passing by value is conceptually simplest, and what was inherited from C. It's a reasonable and sane default. Many people will disagree with your assertion that "most of the time you intend to pass by reference anyway" it just depends on what sort of code you are writing in your specific niche/

1
mredding•

I don't consider it a flaw. OOP doesn't scale. The language isn't an OOP language, it's a multi-paradigm language. Its mostly a Functional language, if you look at essentially every development since streams. In functional programming, mutable state is often considered a flaw. Large structured data is at least a code smell. Listen to what the language is telling you. If it's painful, then stop doing it. There's a lot of bad code out there, and a lot of developers committed to their ways.

1
Ok-Sell8466•

I disagree that modern C++ features make it mostly a functional language. In my opinion it's not even close. But I think I see what you're saying, that more recent features encourage a more functional style.

1
HappyFruitTree•

Sometimes I wonder if people in general really know what "functional programming" really is. It's probably difficult if one hasn't programmed in Haskell or some other pure functional programming language before. It's not just about using what we in C++ call "functions".

1
ABlockInTheChain•

It's too bad that marketing pressures and sloppy definitions created this idea that the various "paradigms" are in conflict with each other. I think it helps to start out by looking at various approaches that have been called "paradigms" and separating out the goal of the paradigm from the specific techniques used to achieve it.

"Imperative" programming is the original and most fundamental style. It only got a dedicated name after other paradigms were invented so we needed a name for it. To the extent it has an design philosophy it's about calculating every computable function. The major downside, and the reason other approaches where invented, is unbounded complexity. Every other approach can be described as adding restrictions in order to tame the unbounded complexity.

Structured programming is about restricting the layout of source code in order to reduce the error rate when it is interpreted and modified by humans. Historically the term was associated with some specific rules relevant to the particular languages where it was first applied, but those shouldn't be thought of as part of the essential concept. The essential concept can still be seen today in Python's syntactically-relevant indentation and Golang's mandatory formatter.

Functional programming is about restricting what functions can do so that it's easier to reason about the consequences of calling them. In particular, it's about forbidding side effects. In order to actually get any non-trivial work done in a language where functions can't have side effects you have to invent several other techniques to make this possible, and those techniques end up having utility above and beyond merely merely making functional programming viable which is why those techniques are often thought of as functional programming itself.

Object oriented programming is about restricting access to data so that related items can't be modified in a way that violates invariants. The atom of invariant preservation is called an "object", and languages which implement objects usually integrate them into the type system and provide ways to express relationships between their types (inheritance). Inheritance isn't the essential concept though, it's just something that usually tags along with it.

Once you separate out the essential concepts from the minutia of how they are implemented it's clear that the question of "which paradigm should I use?" is: "all of them."

You should be able to calculate any computable function, you should restrict source code layout to reduce errors, you should organize data into discrete units that preserve invariants, and you write functions which have as few side effects as possible, ideally none.

1
mredding•

In my opinion it's not even close.

You have me curious. Can you elaborate?

I'm definitely trying to imply what u/ABlockInTheChain is saying, that the paradigms can and do interplay. I'm just suggesting that there is a right tool for every job. But you do see some of what I am saying, that the direction of the language and modern standards hasn't exactly doubled down on OOP concepts exclusively, like there's really any fruit still hanging on those branches.

1
Ok-Sell8466•

Sure. We do have std::function which gives better support for higher order functions, and features like std::optional<T> that are more commonly seen in functional language. However, they feel very clunky and are very clearly an afterthought rather than a core features of the language. We don't really have persistent data structures in the standard collections -- mutability is encouraged.

1

I'm confused. What do you mean with "OOP doesn't scale" and "The language isn't an OOP language"?.

1
mredding•

The 90s were a bad time. They OOP'd all the things, and discovered how really bad literally everyone was at it. Mutable state, and large, complex instances... When processors stopped getting faster, they started getting wider and going multi-core. This model of computing didn't scale with that; it incurred so much mutual exclusion a fair amount of software is at best no faster than single threaded code. Functional programming scales across cores.

I say C++ is not an OOP language because it is a multi-paradigm language. Don't just think about OOP, C++ is not a one trick pony, neither should you be.

1
Dubroski•

Design flaw? I don't think so.

Imagine everything gets passed by reference. How can you guarantee that a function isn't going to modify your variable when you pass it in? What if you don't want it to be possible for that to happen? So you'd have to make up syntax in the language to make it explicit to pass by value. Similar to how you have to make it explicit when you want to pass by reference now.

Passing by value is a safer default as you know by default your variables don't get changed underneath you. I think most other languages are also pass by value by default. Though some may handle objects differently vs native types.

1
xypherrz•

How can you guarantee that a function isn't going to modify your variable when you pass it in?

const reference?

1
Dubroski•

So to have a simple parameters like a Boolean or an int in my parameters I now how to decorate the signatures with const everywhere since they are passed by reference in this scenario? That sounds nightmarish to look at

1
Unlikely-Ad-431•

This has turned into an interesting discussion for this particular post because you organically highlighted an issue that actually is commonly seen as a c++ design flaw: default mutability.

Default pass by value is more a feature than a flaw in my opinion, but default mutability on the other hand…

Having to const everything is not only tedious and ugly, it’s reckless.

1
Dubroski•

Yes exactly. Rust fixes this with immutable by default but I find it a pain having to always use mut everywhere I need it. Similar to how exhausting it is to use const. Though with Rust, it is the safe approach (which is what the language aims for anyways). If only C++ just made references const by default and we marked them otherwise when mutability was necessary

1
robhanz•

They answered how you could validate that your reference wouldn't be modified, they didn't advocate using it everywhere.

I don't know why anybody would pass primitive types by const ref rather than value. Const ref would normally only be used for datatypes that have reasonable overhead for the copy.

1
Dubroski•

Yes which is why I also think it would be a worse design flaw if C++ passed by ref by default.

1
bert8128•

If you pass a bool or int by value then as far as the caller is concerned the variable is being passed const - the callee cannot modify the caller’s version of that variable.

1
tstanisl•

The `const reference` does not guarantee that the pointed object is not going to change. It just disallows to change this object via the given reference. Moreover, the reference can be cast to no-const one and used to change the object. This behavior is defined as long as the original object is not `const` qualified.

1
ButchDeanCA•

Passing by value is not a design flaw because you may have a scenario where the passed in value will need to be amended but you don’t want the parameter passed in the call to the function to be amended - there’s nothing wrong with that.

Like all things in C++, considerations should be well justified.

1
Straight-File9526•

I think this is a feature for functional programming compatible

1
TryToHelpPeople•

It’s not a design flaw, it’s a user choice. You literally have the choice to pass any way you like,

If, outside of function parameters, in your general code, you wrote

std::string name;

You would be creating a value. But if you wrote

std::string& nameref;

You’d be creating a reference. And guess what you’d be creating if you made your reference const?

One of the things I like about C++ is that it’s so explicit.

1
std_bot•

Unlinked STL entries: std::string


^(Last update: 09.03.23 -> Bug fixes)Repo

1
SoerenNissen•

It might - I have never had this thought before now - it might be a design flaw to have a default.

After many decades of use, possibly there's a case to be made that const& ought to have been the default semantics, but I wouldn't call it a flaw to have picked by-value. C#/Java's decision to do it by nonconst reference, now that was a mistake.

1
HappyFruitTree•

Note that many programming languages treat primitive types (int, double, etc.) and user-defined types (classes) differently. Primitive types are passed by value and user-defined types are passed by reference, and you often don't have a choice.

In C++ you do have a choice, and I think that is one of C++'s big strengths. C++ has many inconsistencies but not in this area. You can treat primitive types and user-defined types essentially the same way. It means you don't have to pay the overhead of passing small user-defined types by reference and you can chose to pass primitive types by reference if that makes sense.

One drawback is that it can be a bit too easy to accidentally copy objects without realizing. I really like that C++ makes it easy to make deep copies of (nested) containers but if you do it frequently without realizing then it can become a performance problem. The random number generator (e.g. std::mt19937) should normally not be copied because that ruins the "randomness" (two generators with the same state used in the same way will generate the same "random" numbers).

If pass-by-reference was the default, would that also apply to function return values? That would be very error-prone because you never want to return references to local variables and temporary objects.

Given the kind of language that C++ is (high-performance, no garbage-collection, dangling references, etc.) I think pass-by-value by default was the right decision.

1
std_bot•

Unlinked STL entries: std::mt19937


^(Last update: 09.03.23 -> Bug fixes)Repo

1

no, pass by value should be the way except it should be move by default instead of copy

1
noooit•

Yeah I've seen people accidentally deep copying stuff by mistake, i think it should be more explicit, but it's also a C thing. Although, they pass pointers for structs usually. C++, you also need it to be value for move semantic. I would've prefer that if it was move by default.

1

Where did your misconception about C# come from? You literally need to prefix variables with the ref keyword for reference and out for an outwards return variable…

1
HappyFruitTree•

But in any case, the class objects are not passed by reference, right? It's the references to those objects that are passed by value or by reference, isn't that so?

If the above is correct then it seems true enough to say that the objects gets passed by reference.

1

Only objects, value types are passed by value. And actually deep cloning objects in C# to get around that was more troublesome than passing things by reference in C++ any day.

1
teerre•

Ideally speaking pass-by-value is certainly the correct choice. References semantics are a mistake. Hard to understand. Hard to debug. Worse performance. Good bye concurrency. Modern languages like Swift basically default to pass by value, some experimental languages like Val build their whole rationale around passing by value. Even in C++ itself, "value semantics" is a common term you'll hear if you go any symposium.

That said, the particular implementation of passing by value in C++ doesn't do anyone any favors. Often the reality of the situation is that working with references ends up being a net positive. So was it a mistake? Hindsight is 20/20, but I find hard to argue that considering when C++ was created and how it evolved.

1
no-sig-available•

There is also a third option - let the compiler design.

Some languages let you mark the parameters with in or out (or both) to tell how you intend to use the parameters. The compiler then selects the most effective way to do that.

Sometimes I wish C++ had that option, especially when writing templates and you don't know what type x will have in template<class T> f(T x).

1
shipshaper88•

I think it’s more a side effect of the fact that you have total control in c++.

1
pedersenk•

Many primitives are smaller than the size of a pointer (and reference); so it would be wasteful to copy the larger memory location each time than the smaller value type.

1
joncppl•

Never thought about it, just let clang-tidy remind me when an object as an argument is large or has a non trivial copy constructor. For small trivial types, mov to registers or stack is less overhead than indirection (assuming no inlining), so I think I prefer the default. With experience and good tooling it's not a hard judgment to make and quick to change.

1
TheTomato2•

You always pass by value because a pointer is a value. This how your CPU does it when it pushes a new stack frame which why C does it and also why C++ does it. Doing the opposite way your CPU does it would be the stupid way.

1
QuentinUK•

If the value is no longer needed by the caller.

If you want to modify the value in the subroutine.

Then the value can be moved, e.g. with std::move, which will save copying.

Below you mention “simple parameters like a Boolean or an int in my parameters”, to do everything with these indirectly via references would make things slower. C++ defaults to what is simplest for the processor and if you want you can wrap your int’s in objects and keep them on the heap.

1