Firstly, it's worth noting that this design did not originate in C; it's entirely new in C++.
For the same syntactic reason, it is permitted to take the address of a library function even if it is also defined as a macro.89
89 This means that an implementation must provide an actual function for each library function, even if it also provides a macro for that function.
- C89 Standard, 4.1.6 Use of library functions
These strict guarantees would be very restrictive in C++ though, for a number of reasons.
As a disclaimer, I haven't been able to find quotes from Bjarne himself, so everything that I'm about to say is a collection of community consensus and personal experience.
1. Adding overloads may break source compatiblity
Say you have a function:
bool is_even(int x) { return x % 2 == 0; }
It may be initially safe to call std::partition(begin, end, is_even)
, but if an overload for long
and long long
was added in addition to int
, then the use of is_even
would become ill-formed.
Essentially, any addressable function cannot receive extra overloads in the future because it breaks existing code. This is why [namespace.std] specifically says "possibly ill-formed".
2. Signatures are more prone to change than in C
Another way to break compatibility is to make an existing function more generic.
For example, it lets the standard library make its functions more generic, such as turning:
// possible historical implementation in <math.h> until C++11
#define isnan(x) implementation-defined
into
bool isnan(long double); //possibly with overloads for float and double
and subsequently into
bool isnan(std::floating_point auto);
With features such as function overloading and templates, the implementation of a function can change drastically over time.
Of course, no one could have foreseen these drastic changes in the math library, but the restrictions on non-addressable functions have made them possible without breaking any conforming code.
3. Functions may not have an address
There are two possible reasons why a function might not have any address:
- it is an intrinsic function
- this means a function call just tells the compiler to produce some IR instructions, and an actual function might not even exist
- it is an immediate function (i.e.
consteval
, C++20)
The former reason may have been a significant contributor to the decision.
Nowadays, there is usually an inline
function wrapper around any intrinsics used in the standard library, but this would turn into common practice was not obvious back in the day.
A more modern example of intrinsic standard library functions is std::move
, which was made "kinda intrinsic" in the MSVC STL.
See Improving the State of Debug Performance in C++.
4. "Functions" may be function objects
In C++, it is also possible to implement a function as a function object, such as:
// inline has only been added in C++17, but compilers could have supported its
// functionality before that
inline const struct {
float operator()(float x) const;
} sqrt;
If a function is actually a function object, then there would be a difference in behavior when taking its address, as you wouldn't get a function pointer. However, calling it would behave the same (except ADL doesn't take place).
This is yet another form of flexibility that is made possible by making functions non-addressable.
Conclusion
Making it possible to take the address of standard library functions would have significantly reduced the flexibility of implementers.
Almost any change, such as adding overloads would break compatibility, effectively freezing language progress.
This is not a big issue in C, where function signatures are frozen anyway, but would have significant negative consequences in C++.
foo(1,1)
, but that does not necessarily imply that there is such a functionvoid (int,int)
, it could bevoid(int,int,int=0)
or something else entirely – Lingcodlanguage-lawyer
tag and uselanguage-design
instead. This isnt an answer you can find by reading and lawyering about the standard – Lingcodconstexpr
array of function pointers --I wonder if this rationale of compiler intrinsics being easier to inline was previously valid but is now just out of date. – Precedency_mm256_zeroall()
, it's injecting the SSE2 assembly instructions in place. There is no function; it's not inlining a function. It's more like (very old school) a bunch ofemit
code that blindly outputs arbitrary bytes into the function (presumably those bytes are carefully curated machine code bytes). – Spreadeaglestd::copy
can be implemented more efficiently as an intrinsic. Why not have__builtin_std_copy
as the intrinsic andstd::copy
as an inline wrapper around that? – Precedency