Many programming languages, including C and C++, chose the traditional mathematical definition of functions as their model for pure functions:
A function is a process or a relation that associates each element x of a set X, the domain of the function, to a single element y of another set Y (possibly the same set), the co-domain of the function.
Many early programming languages, as well as most later languages that were heavily influenced by them, have chosen this approach. From a mathematical point of view, it makes sense. Syntactically, in C or C++, you can only specify one return value (or no return value, if you specify void as the return type).
Strictly speaking, while C and C++ limit officially you to returning one value from a function, there are still ways to return multiple values from a function in these languages.
Let’s first focus on just the return type itself.
There is nothing in C that prevents you from returning a struct or a pointer to a struct. Of course, a struct can contain multiple members of any data type, including arrays with one or more dimensions. Likewise, there is nothing in C++ that prevents you from returning either a struct or object (instance of a class), or a pointer to a struct or object, or a reference to a struct or object. C++ structs and classes can contain multiple members of any data type, and the methods (member functions) to access them. So, returning a single “value” can mean returning multiple values, if you specify a struct or class (or a pointer to a struct or class) as the return data type from a function. And a function in either language can return a pointer to an array which was dynamically allocated and initialized.
While not necessarily adhering to the spirit of a pure function, this approach can be considered a pure function because you’re just returning one “value.” Under the covers, however, that value can consist of many pieces.
Now, let’s consider other ways to return information from a function to its caller, be removing the restriction of using a pure function and stepping into the world of impure functions.
One way to return information to the caller is by changing the value(s) of one or more global variables or data structures (or causing them to be changed, indirectly). In other words, the function can have side effects that have nothing to do with its official return value. There are some limited scenarios in which this approach might make sense, but this approach is generally discouraged because it can lead to bugs that can be subtle and difficult to debug. So, I won’t dwell on this one. Think very carefully about all the implications before you even consider it. Then, think again.
A common approach to returning multiple values from a function involves defining output (or input/output) parameters. Using a pointer parameter in C or C++, or using a reference parameter in C++, a function can pass information back to the caller. Again, the information could be a simple numeric value, or could contain many values, arrays, etc. inside a struct or object. Given that C11 requires a conforming C compiler implementation to allow at least 127 parameters in a function definition (ISO/IEC 9899:2011, section 188.8.131.52), and the C++17 standard suggests that a C++ compiler implementation allow at least 256 parameters in a function definition (ISO/IEC 14882:2017, Annex B 2.11), there is plenty of room for passing information back to the caller through output parameters. (If you define a function that has anywhere near 127 parameters, though, I recommend rethinking your design.)
So, although C and C++ appear to allow only a single value to be returned by a function, the possibilities of passing information back to the caller are effectively limitless, for all practical purposes.
There are several other languages which explicitly allow returning multiple values from a function.