Date: Thu, 10 Apr 2025 09:53:52 +0100
Let's say I give you a code test, and I ask you to write the body of
the following function as efficiently as possible:
template<typename T>
bool ProcessNode(T &&arg)
{
// write code here
}
This function does only one thing, it invokes the "AssimilateNode"
function on the argument. So the best way to do this would be:
template<typename T>
bool ProcessNode(T &&arg)
{
AssimilateNode( forward<T>(arg) );
}
But now I tell you that the function must invoke three functions on
the argument. It must normalise the node, then it must distribute the
node, and finally it must assimilate the node. So then the code
becomes:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode ( forward<T>(arg) );
DistributeNode( forward<T>(arg) );
AssimilateNode( forward<T>(arg) );
}
But of course now we have a problem here. After we have invoked
'NormaliseNode', giving it an Rvalue, the argument might no longer be
a valid object; it might have had its resources plundered by a new
object. So we must change the above code to the following:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode ( arg );
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
The fact that we can only use 'forward' on the last use of the
argument inside the body of the function, is the most major point of
debate as to why the 'forwarding' should not be done automatically --
it is the reason why we must explicitly write "std::forward< ... > (
... )" in our code. Imagine for a moment if we were to use the
trigraph operator '&&&' to indicate that a function parameter must
undergo automatically forwarding, something like this:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph &&& means automatic forwarding
{
NormaliseNode (arg); // automatically forwarded
DistributeNode(arg); // automatically forwarded
AssimilateNode(arg); // automatically forwarded
}
We have the same problem here: If 'NormaliseNode' is invoked with an
Rvalue, then we might be left with an invalid object (i.e. a
plundered-from object) which then gets passed to 'DistributeNode'.
So here's my idea:
We can have automatic perfect forwarding if the compiler can correctly
identify the last mention of the variable in the body of the function,
and only perform forwarding on the last mention. What I mean is that
when the compiler encounters the following code:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph &&& means automatic forwarding
{
NormaliseNode (arg);
DistributeNode(arg);
AssimilateNode(arg);
}
The compiler work as follows:
Step 1 - It notices the use of the trigraph '&&&' in the function
parameter, and so it knows to perform automatic forwarding on 'arg'.
Step 2 - It reads through the function and identifies the last mention
of the variable 'arg', and it applies 'std::forward< ... > ( ... )' to
the last usage.
Step 3 - All other mentions of the variable 'arg' don't get forwarded.
So essentially the compiler takes the above function and converts from
trigraph '&&&' to digraph '&&' as follows:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode ( arg );
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
Now of course, code can be a little more complicated if it branches.
We could have a function body such as the following:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph &&& means automatic forwarding
{
NormaliseNode(arg);
if ( 5 == (arg.id & 7u) ) // check if it's a nomadic node
{
SettleNode(arg);
return;
}
DistributeNode(arg);
AssimilateNode(arg);
}
So basically what the compiler has to do here is identify each
possible return path from the function, and work backwards from the
return path to identify the last mention of the variable. So the above
function written with tripgraph '&&&' would be converted to digraph
'&&' as follows:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode(arg);
if ( 5 == (arg.id & 7u) ) // check if it's a nomadic node
{
SettleNode( forward<T>(arg) );
return;
}
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
There are some scenarios though in which the 'forwarding' can be
wasted. Consider the following function:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph for automatic forwarding
{
NormaliseNode (arg);
DistributeNode(arg);
if ( NodeManager::mode != NodeManager::ModeAssimilate ) return;
AssimilateNode(arg);
}
When the compiler converts this function body from trigraph '&&&' to
digraph '&&', it changes it to:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode (arg);
DistributeNode(arg);
if ( NodeManager::mode != NodeManager::ModeAssimilate ) return;
AssimilateNode( forward<T>(arg) );
}
You can see the inefficiency here: If the Node Manager is not
currently in Assimilation Mode, then the invocation of the
'ProcessNode' function will never forward the argument. This is where
we as the programmer must be aware of how branching affects automatic
forwarding. So we would have to rewrite the above trigraph function as
follows:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph for automatic forwarding
{
NormaliseNode (arg);
if ( NodeManager::mode == NodeManager::ModeAssimilate )
{
DistributeNode(arg);
AssimilateNode(arg);
}
else
{
DistributeNode(arg);
}
}
which the compiler would then covert to digraph as follows:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode(arg);
if ( NodeManager::mode == NodeManager::ModeAssimilate )
{
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
else
{
DistributeNode( forward<T>(arg) );
}
}
You might think this is a little tedious and repetitive to have to
split the function into two branches which two distinct invocations of
the function 'DistributeNode', but we would have to do this with
digraph references too, so it's not like the introduction of trigraph
references are making anything more complicated here.
So if trigraph references were to be adopted into the Standard, we
would give compiler writers some time to implement them. Then after a
few years, we could go further with it, and specify that the compiler
must automatically improvise the branching -- specifically what I'm
saying here is that when the compiler encounters the following code:
template<typename T>
bool ProcessNode(T &&&arg) // Trigraph for automatic forwarding
{
NormaliseNode (arg);
DistributeNode(arg);
if ( NodeManager::mode != NodeManager::ModeAssimilate ) return;
AssimilateNode(arg);
}
then it must treat it as though it were written:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode(arg);
if ( NodeManager::mode == NodeManager::ModeAssimilate )
{
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
else
{
DistributeNode( forward<T>(arg) );
}
}
I realise that this last feature would be a lot more work for compiler
vendors, and so it wouldn't be a part of the first specification for
trigraph references. This last feature would be in a follow-up paper a
few year after compiler vendors have had some time to implement the
initial more-simplistic idea of trigraph references.
the following function as efficiently as possible:
template<typename T>
bool ProcessNode(T &&arg)
{
// write code here
}
This function does only one thing, it invokes the "AssimilateNode"
function on the argument. So the best way to do this would be:
template<typename T>
bool ProcessNode(T &&arg)
{
AssimilateNode( forward<T>(arg) );
}
But now I tell you that the function must invoke three functions on
the argument. It must normalise the node, then it must distribute the
node, and finally it must assimilate the node. So then the code
becomes:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode ( forward<T>(arg) );
DistributeNode( forward<T>(arg) );
AssimilateNode( forward<T>(arg) );
}
But of course now we have a problem here. After we have invoked
'NormaliseNode', giving it an Rvalue, the argument might no longer be
a valid object; it might have had its resources plundered by a new
object. So we must change the above code to the following:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode ( arg );
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
The fact that we can only use 'forward' on the last use of the
argument inside the body of the function, is the most major point of
debate as to why the 'forwarding' should not be done automatically --
it is the reason why we must explicitly write "std::forward< ... > (
... )" in our code. Imagine for a moment if we were to use the
trigraph operator '&&&' to indicate that a function parameter must
undergo automatically forwarding, something like this:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph &&& means automatic forwarding
{
NormaliseNode (arg); // automatically forwarded
DistributeNode(arg); // automatically forwarded
AssimilateNode(arg); // automatically forwarded
}
We have the same problem here: If 'NormaliseNode' is invoked with an
Rvalue, then we might be left with an invalid object (i.e. a
plundered-from object) which then gets passed to 'DistributeNode'.
So here's my idea:
We can have automatic perfect forwarding if the compiler can correctly
identify the last mention of the variable in the body of the function,
and only perform forwarding on the last mention. What I mean is that
when the compiler encounters the following code:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph &&& means automatic forwarding
{
NormaliseNode (arg);
DistributeNode(arg);
AssimilateNode(arg);
}
The compiler work as follows:
Step 1 - It notices the use of the trigraph '&&&' in the function
parameter, and so it knows to perform automatic forwarding on 'arg'.
Step 2 - It reads through the function and identifies the last mention
of the variable 'arg', and it applies 'std::forward< ... > ( ... )' to
the last usage.
Step 3 - All other mentions of the variable 'arg' don't get forwarded.
So essentially the compiler takes the above function and converts from
trigraph '&&&' to digraph '&&' as follows:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode ( arg );
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
Now of course, code can be a little more complicated if it branches.
We could have a function body such as the following:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph &&& means automatic forwarding
{
NormaliseNode(arg);
if ( 5 == (arg.id & 7u) ) // check if it's a nomadic node
{
SettleNode(arg);
return;
}
DistributeNode(arg);
AssimilateNode(arg);
}
So basically what the compiler has to do here is identify each
possible return path from the function, and work backwards from the
return path to identify the last mention of the variable. So the above
function written with tripgraph '&&&' would be converted to digraph
'&&' as follows:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode(arg);
if ( 5 == (arg.id & 7u) ) // check if it's a nomadic node
{
SettleNode( forward<T>(arg) );
return;
}
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
There are some scenarios though in which the 'forwarding' can be
wasted. Consider the following function:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph for automatic forwarding
{
NormaliseNode (arg);
DistributeNode(arg);
if ( NodeManager::mode != NodeManager::ModeAssimilate ) return;
AssimilateNode(arg);
}
When the compiler converts this function body from trigraph '&&&' to
digraph '&&', it changes it to:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode (arg);
DistributeNode(arg);
if ( NodeManager::mode != NodeManager::ModeAssimilate ) return;
AssimilateNode( forward<T>(arg) );
}
You can see the inefficiency here: If the Node Manager is not
currently in Assimilation Mode, then the invocation of the
'ProcessNode' function will never forward the argument. This is where
we as the programmer must be aware of how branching affects automatic
forwarding. So we would have to rewrite the above trigraph function as
follows:
template<typename T>
bool ProcessNode(T &&&arg) // trigraph for automatic forwarding
{
NormaliseNode (arg);
if ( NodeManager::mode == NodeManager::ModeAssimilate )
{
DistributeNode(arg);
AssimilateNode(arg);
}
else
{
DistributeNode(arg);
}
}
which the compiler would then covert to digraph as follows:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode(arg);
if ( NodeManager::mode == NodeManager::ModeAssimilate )
{
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
else
{
DistributeNode( forward<T>(arg) );
}
}
You might think this is a little tedious and repetitive to have to
split the function into two branches which two distinct invocations of
the function 'DistributeNode', but we would have to do this with
digraph references too, so it's not like the introduction of trigraph
references are making anything more complicated here.
So if trigraph references were to be adopted into the Standard, we
would give compiler writers some time to implement them. Then after a
few years, we could go further with it, and specify that the compiler
must automatically improvise the branching -- specifically what I'm
saying here is that when the compiler encounters the following code:
template<typename T>
bool ProcessNode(T &&&arg) // Trigraph for automatic forwarding
{
NormaliseNode (arg);
DistributeNode(arg);
if ( NodeManager::mode != NodeManager::ModeAssimilate ) return;
AssimilateNode(arg);
}
then it must treat it as though it were written:
template<typename T>
bool ProcessNode(T &&arg)
{
NormaliseNode(arg);
if ( NodeManager::mode == NodeManager::ModeAssimilate )
{
DistributeNode( arg );
AssimilateNode( forward<T>(arg) );
}
else
{
DistributeNode( forward<T>(arg) );
}
}
I realise that this last feature would be a lot more work for compiler
vendors, and so it wouldn't be a part of the first specification for
trigraph references. This last feature would be in a follow-up paper a
few year after compiler vendors have had some time to implement the
initial more-simplistic idea of trigraph references.
Received on 2025-04-10 08:54:04