return
statement is seemingly very basic, but in fact, one of the most puzzling aspects of procedural programming for freshman students in our local university.
What are the functions anyway?
Functions are the building blocks of procedural and functional programming languages. You might think of a function as of reusable, maintainable block of code responsible for some action. If you perform some operations often together, you might want to "pack" them inside a function. A "good" function is a little machine that takes some input data and gives back processed data.
In C++ you can "share" the results of the processing with "outside code" by return
ing a value, modifying a parameter (worse) or modifying the global state (worst).
In simple case, "returning" functions are direct analogs of math functions like y = f(x)
(even the call syntax is very similar). As a programmer you just define what is x
, what is y
and how exactly f
maps x
to y
.
Printing vs returning
Now, the printing into console (terminal) and return
ing are not the same. In simple words, printing is just showing the user some characters on the screen ("speaking with user"). Returning from function allows to receive the result from function ("speaking with outside code"), but it's invisible for the user unless you print it afterwards .
Different function layouts you may encounter while learning
(does not pretend to be an exhaustive list)
So, in your class or tutorial sometimes they teach you how to
(1) print an object directly within main()
int main() {
int i = 42 + 13;
std::cout << i << std::endl;
}
(2) print an object inside a function
void printSum(int a, int b) {
int i = a + b;
std::cout << i << std::endl;
}
int main() {
printSum(42, 13);
}
(3) return an object from the function and printing afterward
void sum(int a, int b) {
return a + b;
}
int main() {
int s = sum(42, 13);
std::cout << s << std::endl;
}
Obviously, (1) is not reusable at all: to change parameters you must intervene into the program logic. Also, if logic is something more than just a sum, main()
function will grow quickly in size and become unmaintainable. Responsibilities will be scattered across the code, violating Single responsibility principle
(2) is better in all aspects: function encapsulates logic in a separate place in code, has distinctive name, can be changed separately from main function. Function can be called with different values, and changing arguments doesn't change the function.
But it still has 2 responsibilities: perform the calculation and output to the screen. What if you want not to print the result, but write it to a file? or send it via network? or just discard it? You will need to write another function. The logic inside this new function will be the same, thus introducing duplication and violating DRY principle. Also, (2) is not a pure function because it is modifying std::cout
which is basically the global state (we say that this function has "side effects").
Also, you can think about (2) as if it was (1), but with whole program logic moved from main()
into a separate function (that's why sometimes functions are called "subprograms").
(3) solves the multiple responsibility problem, by getting rid of printing (moving it into the "user code"). The function contains only pure logic. Now it's a number crunching machine. In main function you can decide what to do with the result of the calculations without touching the calculating function. It becomes a "black box". No need to worry how it is implemented, and no need to change anything, it just works.
In your post there is a shorter version of (3):
int main() {
std::cout << sum(42, 13) << std::endl;
}
The difference is that no temporary object int s
being created, but return value is being written directly to std::cout
(in fact, passed as a parameter to a function called operator<<)
In real life
In real-life programming, most of the time you will be writing functions like (3), that don't print anything. Printing to terminal is just a quick and simple way to visualize data. That's why you've been taught how to output to standard streams rather than writing to the files, network sockets or showing GUI widgets. Console printing is also very handy during debugging.
See also:
P.S. There is a big deal of simplification in this post, thus the usage of lots of quoted terms.