What does f(x) mean in C++?
Intro
It is universally agreed that C++ is a complex language. One reason is that its syntax is highly overloaded, meaning that the same code could mean many different things. Let’s do a simple mental exersice by considering the following code:
f(x)
How many possible meanings can there be?
Macros
First, let get macros out of the box. If any of f
and x
is a macro, f(x)
can expand to basically anything.
Hereafter, we presume that f
and x
are not macros.
Function Call
f(x)
can be a call to function named f
with argument x
. Duh.
void f(int);
int x = 0;
f(x); // call to f. Simple as that.
There are a few sub cases:
-
If
f
is a function template, then template argument deduction takes place, and implicit template instantiation follows. The compiler automatically generates a specialization (f<int>
) for you. -
If
f
is an overload set, then overload resolution takes place. The compiler picks the best candidate for you.
Note that 1 and 2 can both happen in a single call.
Indirect Function Call
If f
is a pointer to function, then f(x)
is the same as (*f)(x)
. This is known as “indirect function call”.
The indirection comes from “deferencing” the pointer to function to get the actual function.
void g(int);
auto f = &g; // f has type void (*)(int)
auto f = g; // same as above; function decay
int x = 0;
f(x); // g(x)
(*f)(x); // same as above
This is a very powerful technique, and is one way to get polymorphism.
Similarly, when f
is a reference to function (yes, functions can have references):
void g(int);
auto& f = g; // f has type void (&)(int)
int x = 0;
f(x); // g(x)
Call Operator
If f
is an object whose type provides operator()
, then f(x)
invokes that call operator. Such f
is commonly referred to as a function object or simply functor:
struct F {
void operator()(int); // (*)
};
F f;
int x = 0;
f(x); // calls (*)
Lambdas, std::function
, and the return type of std::bind
/std::bind_front
/std::bind_back
, are all examples of such.
Just like free functions, this operator()
can be overloaded and templated.
- Generic lambdas have templated
operator()
Surrogate Function
On the other hand, a type can define a conversion operator that converts the instance to a function pointer, thus making it “callable”. That is:
void g(int);
struct F {
using Fun_Pointer = void(*)(int);
operator Fun_Pointer() { return g; }
};
F f;
int x = 0;
f(x); // g(x)
This is known as “Surrogate Function”.
- A captureless lambda is also a surrogate function, because it can convert to a function pointer
Object Creation
If f
is a type (or an alias to a type) and x
is an object, then f(x)
creates a new object by calling the constructor:
struct f {
f(int);
};
int x = 0;
f(x); // (1)
(1)
creates a temporary object and immediately destroys it.
Right?
Wrong.
The above code doesn’t compile. Here’s what the compiler says:
<source>:19:11: error: conflicting declaration 'main()::f x'
19 | f(x); // (1)
| ^
<source>:18:13: note: previous declaration as 'int x'
18 | int x = 0;
(1)
is actually a definition - it defines a variable x
of type f
. It is the same as f x;
. The parentheses are redundant, but permitted nonetheless.
struct f {};
f(x); // Ok: declare a variable `x` of type `f`, then default-initialize it
The parentheses are even allowed in function parameter list, which I don’t even want to talk about:
void fun( int (x), int (y) ); // Why would anyone write it this way?
Anyway, to make f(x)
do what we thought it’d do, we can wrap it with a pair of parentheses to make it an expression:
struct f {
f(int);
};
int x = 0;
(f(x)); // Create a temporary object and immediately destroys it
What about just f(x);
? Can this create an (temporary) object?
Yes, if f
is a class template, and class template argument deduction (CTAD) kicks in:
template <class T>
struct f {
f(T);
};
int main() {
int x = 0;
f(x); // (?)
}
Update: Since this post was published, r/sagittarius_ack pointed it out that the above code doesn’t compile, at least on GCC, Clang and MSVC, due to conflicting definition. It does compile on EDG though - EDG treats the line (?)
as an expression statement that creates a temporary object and immediately destroys it.
Even more interestingly, consider the following code:
#include <iostream>
template <class T = int>
struct f {
f() { std::cout << "1"; }
f(T) { std::cout << "2"; }
~f() { std::cout << "3"; }
};
int x = 0;
int main() {
f(x); // (?)
}
There is implementation divergence:
- GCC 14.2 accepts it, and prints
13
- This suggests that GCC treats line
(?)
as a (shadowing) declaration, same asf x;
- This suggests that GCC treats line
- Clang 19.1.0 rejects it
- MSVC v19.40 rejects it
- EDG 6.6 accepts it, but prints different output
23
- This suggests that EDG treats line
(?)
as an expression, same as(f(x));
- This suggests that EDG treats line
I’m not sure what’s going on.
Invoke Static Call Operator?
Since C++23, call operators are allowed to be static
:
struct f {
static void operator()(int); // Ok since C++23
};
So, does this make f(x)
an invocation to that static method, given x
is an int
?
No.
The correct way to call that static method is the same as how you usually call a static method - use the ::
scope resolution:
f::operator()(x); // Ok since C++23
By the way, C++ has always allowed you to invoke a static method through an instance (even it is not used at all):
f v;
v(x); // Ok
In short, despite its look, f(x)
is never a call to the static call operator of type f
.
- Similarly, despite
operator[]
is allowedstatic
, you cannot call it likeT[arg]
. You have to writeT::operator[](arg)
.
Function Type
If both f
and x
are types, then f(x)
is a function type - functions that take an x
and return an f
.
struct f {};
struct x {};
f(x); // (1)
The above code compiles, so (1)
must have formed a function type.
Right?
Wrong.
C++ does not allow “empty declarations” - declaration does not declare anything. In short, this is illegal:
int; // error: declaration does not declare anything
But f(x);
compiles. Are function types an exception to the rule?
No. Here, f(x);
defines a new variable called x
, same as before. Even though x
is already a type prior to the line.
Yes, you read it right. For whatever reasons, C++ allows (re)using class names as variable names:
class Bar {}; // Bar is a class
Bar b; // Ok, define a variable `b`
int Bar = 0; // Bar is an object now!
Bar b2; // Error
// If you really want the class Bar back:
class Bar b2; // Ok, define a variable `b2`
f(x)
is interpreted as a function type elsewhere, when a type is expected:
using F = f(x); // Ok: declare a type alias F that refers to the function type
std::function<f(x)> ff; // Ok
Just not f(x);
.
What we have So Far
We can produce a summary so far:
-
If
f
is a function, function template, or pointer to function, thenf(x)
is a function call -
If
f
is an object, thenf(x)
either invokes the call operator (static or not), or invokes the conversion operator that returns a pointer to function and calls that instead -
If
f
is a class type, thenf(x);
declares an objectx
of typef
, regardless of whatx
is -
If
f
is a class type andx
is an object, andf(x)
is part of an expression, thenf(x)
creates a temporary object via calling the constructor -
If
f
is a class type andx
is a class type, thenf(x)
is a function type -
If
f
is a class template andx
is an object, thenf(x)
creates a temporary object, provided that class template argument deduction succeeds; this is even the case forf(x);
Could there be more?
Argument Dependent Lookup (ADL)
This is a special case of calling a regular function. Basically:
namespace my {
struct Bar {};
void f(Bar) {}
}
int main() {
my::Bar x;
f(x); // Ok; same as my::f(x)
}
Note that the f
is unqualified, yet the compiler finds my::f
because the argument x
, a my::Bar
, brings in everything under the namespace my
.
What this means is, f(x)
can mean ns1::f(x)
, ns1::ns2::f(x)
, and so on.
Conversion Operator
When f
is a type (or a type alias), and x
has a conversion operator to f
, f(x)
may call that conversion operator:
struct f;
struct X {
operator f(); // (2)
};
X x;
auto y = f(x); // Calls (2); `y` is an `f`
Then again, as we’ve seen more than once already, the following is a declaration:
f(x); // Declare a variable `x` of type `f`, then default-initialize it
Functional-style Cast
Generally, when f
is a type (or a type alias), f(x)
is the syntax for function-style cast - cast the expression x
to target type f
.
As you can see, it gets the name because it looks like a function call, but is not necessarily a function call. For example, the pointer cast that we all like to do:
class T {
...
} object; // An object ...
T* x = &object; // and a pointer to it
using f = const unsigned char*;
auto p = f(x); // No function call happening; it's just a cast
// Now we can poke around the object's internals like a pro
C-style casts, of which function-style cast is one form, despite its perceived simplicity and elegancy, are generally adviced against using (in fact, all explicit casts are recommended to keep to a minimum). There is actually a lot of mechanics going on behind the scenes - C-style casts can do a combination of const_cast
, static_cast
, reinterpret_cast
- and is even an active issue and evolving as of this writing.
- I would only use C-style casts for fundamental types -
int
,double
, and the like. Pointers are excluded.
Take Away
Now comes the summary, take two. Roughly, f(x)
can mean these different things:
- Some sort of function call to
f
- A cast to type
f
(including calls to constructor and conversion operator) - A function type
- A declaration of
x
4
is really surprising and should be avoided, 3
is related to 1
. That leaves us only 2 and 1.
f(x)
should mean a function call. Period. The fact that it sometimes means a declaration is unfortunate; 4
should be avoided. 3
is really a “prototype” of 1
. Now, if C-style casts are replaced with C++ casts (const_cast
, static_cast
, reinterpret_cast
), then f(x)
can really, really mean a function call in the codebase.
Or maybe I missed something?