In older C++ (pre C++11), there is no significant difference between your two implementations of operator+
. Either the copy constructor gets called when you invoke the operator or when you make the explicit copy inside the operator.
In this case, it is more of a personal preference/coding guideline issue which one to choose.
With the introduction of move constructors in C++11, the pass-by-value case has gained an advantage, because that allows the parameter to be constructed using either the copy constructor or the move constructor, depending on what the operator is being invoked with.
For your Vec3i
class, the difference is not that big, but if you have classes that maintain dynamically allocated resources, the proper use of move constructors can significantly reduce the amount of memory that your application needs.
Asking why microcontrollers like the AVR family let you twiddle the pins directly when an operating system like Linux requires device drivers is an apples-to-suspension-bridges comparison.
Microcontrollers are typically single-threaded free-for-alls with no supervisor (OS) and all code has full access to memory, I/O and anything else the chip offers. This means that if one part of the application sets GPIO pin 3 high and needs to see that it stays that way, there's nothing that prevents some other part of the code from setting it low again. The developer is responsible for putting those kinds of interlocks into place. There's also no notion of devices: the MCU has a set of registers that control I/O that's the same on every copy of the chip and that's all there is. If you want to run your program on a different chip in the same family with different I/O, you're likely going to have to rewrite parts of it or, at the very least, recompile it with chip-specific headers or libraries.
General-purpose processors like the one in your desktop PC don't have the I/O features built in, opting instead to communicate with external peripherals whose registers are mapped somewhere in memory. The model is the same, though. If you plop an x86 CPU into a board where you know the I/O mappings, it's entirely possible to write AVR-style programs that do their own I/O, but you're going to be doing it on bare metal without any of the facilities an operating system provides.
A lot of software was done this way, but at some point in history, it became apparent that the model wasn't going to scale. People wanted to write programs to run on a given processor without having to fuss with building a separate version for each combination of peripherals. This spawned operating systems, which let a program say things like "send the character C
across serial port 2" using a standard software interface. The code behind that standard interface was built up for the specific set of hardware on that system and knew that serial port 2 was a Brand X UART mapped at address 1234. In microcomputing, this kind of thing became most evident with the appearance of the CP/M operating system in the 1970s. Vendors selling computers would provide a version of the OS with custom drivers for their specific hardware. Do-it-your-selfers had to patch up generic versions by hand to work with what they had. The result was that they all ended up with systems that could run off-the-shelf versions of software without having to modify it.
Modern operating systems do pretty much the same thing but with a much higher level of sophistication. Systems (PC-style and even many embedded ones) are built around I/O architectures that are enumerable and dynamic, meaning that the software can figure out what's present, assign it a memory mapping and use the right functions to communicate with it. Along with that comes driver infrastructure that lets hardware developers leverage all of that sophistication without having to re-invent it each time and without risk that driver A was going to conflict with something driver B is doing.
If you decide to try going the poke-the-device-directly route on a full-blown operating system like Linux, the first question you'll ask is how a userland process gains access to the memory area where the device's registers are mapped. The answer ("you can't") is what brings up the other reason: multi-user, multi-tasking operating systems don't trust user programs to be well-behaved. They're designed to protect the system from harm and the programs running on the system from each other. Making this happen brings with it a complex model of interlocks and permissions, all of which contribute to whether or not a program can ask a device driver to do something on its behalf. The device driver code put into the kernel is given a much greater amount of trust because, presumably, those who wrote it did so carefully.
The functions you put into a device driver for most flavors of Unix are just the essentials (load, unload, open, close, read, write, ioctl) and are called at the end of a very long chain of decision making. If a process wants to write to a serial port, the driver code isn't called until the OS is sure there's a driver present and functioning for that device, the process has permission to use the device and that the port has been opened by that process.
This, at long last, is why you write a device driver: The amount of code you have to develop for a device driver is minuscule compared to the amount of sophistication the rest of the OS provides to go with it.
Really, it isn't painful. It's just unfamiliar. Once you've done a couple, you'll wonder why you were making such a fuss about it. (And trust me, it's a lot less work now than it was 25 years ago.)
Best Answer
When reviewing code, I apply the following rules:
Always use
const
for function parameters passed by reference where the function does not modify (or free) the data pointed to.Always use
const
for constants that might otherwise be defined using a #define or an enum. The compiler can locate the data in read-only memory (ROM) as a result (although the linker is often a better tool for this purpose in embedded systems).Never use const in a function prototype for a parameter passed by value. It has no meaning and is hence just 'noise'.
Where appropriate, use
const volatile
on locations that cannot be changed by the program but might still change. Hardware registers are the typical use case here, for example a status register that reflects a device state:Other uses are optional. For example, the parameters to a function within the function implementation can be marked as const.
or function return values or calculations that are obtained and then never change:
These uses of
const
just indicate that you will not change the variable; they don't change how or where the variable is stored. The compiler can of course work out that a variable is not changed, but by addingconst
you allow it to enforce that. This can help the reader and add some safety (although if your functions are big or complicated enough that this makes a great difference, you arguably have other problems). Edit - eg. a 200-line densely coded function with nested loops and many long or similar variable names, knowing that certain variables never change might ease understaning significantly. Such functions have been badly designed or maintened.Problems with
const
. You will probably hear the term "const poisoning". This occurs when addingconst
to a function parameter causes 'constness' to propagate.Edit - const poisoning: for example in the function:
if we change
str
toconst
, we must then ensure thatfuction_b
also takes aconst
. And so on iffunction_b
passes thestr
on tofunction_c
, etc. As you can imagine this could be painful if it propagates into many separate files/modules. If it propagates into a function that cannot be changed (eg a system library), then a cast becomes necessary. So sprinklingconst
around in existing code is perhaps asking for trouble. In new code though, it is best toconst
qualify consistently where appropriate.The more insidious problem of
const
is that it was not in the original language. As an add-on it doesn't quite fit. For a start it has two meanings (as in the rules above, meaning "I'm not going to change this" and "this cannot be modified"). But more than that, it can be dangerous. For example, compile and run this code and (depending upon the compiler/options) it may well crash when run:strchr
returns achar*
not aconst char*
. As its call parameter isconst
it must cast the call parameter tochar*
. And in this case that casts away the real read-only storage property. Edit: - this applies generally to vars in read-only memory. By 'ROM', I mean not just physical ROM but any memory that is write-protected, as happens to the code section of programs run on a typical OS.Many standard library functions behave in the same way, so beware: when you have real constants (ie. stored in ROM) you must be very careful not to lose their constness.