Type Decay





5.00/5 (1 vote)
Type Decay
I have previously written about code rot (code decay). This post is about decay in a different context. Essentially, there are three sets of types in C++ that will decay, lose information. This entry will describe the concept, the circumstances, and in some cases ways to avoid type decay from occurring. This is an important topic for me to cover because the addition of support for arrays in Alchemy would have been much more difficult without knowledge of this concept.
Type Decay
Why do certain types decay? Maybe because they have a short half-life?! I actually do not know the reasoning behind all of the rules. I suspect they exist mostly to help things run much smoother. Type decay is a form of syntactic sugar. This is because the original type, T1
, is attempting to be used in a context that does not accept that type. However, it does accept a type T2
, that T1
can be converted to.
Generally, the circumstances involve attempting to use a type T1
, in an expression, as an operand, or initializing an object that expects a type T2
. There are other special cases such as a switch
statement where T2
is an integral type or when the expression T2
reduces to a bool.
The rules are quite involved. For details on the rules for order of conversion, I recommend the page on Implicit Conversions[^] at cppreference.com.
Lvalue Transformations
I am only going to delve into the implicit cast scenarios that relate to Lvalue
transformations. This may sound redundant, but an Lvalue
transformation is applied when an lvalue
argument is used in a context where an rvalue
is expected. Well, it's a lot more redundant if you substitute T1
for lvalue
and T2
for rvalue
.
Briefly, an lvalue
is a type that can appear on the left hand of an assignment expression. In order for a type to qualify as an lvalue
, it must be a non-temporary object or a non-temporary member function. This basically says that a data type with storage will exist when the time comes to write to storage.
An rvalue
is just the opposite. It is an expression that identifies a temporary object and is not a value associated with any object. Literal values and function call return values are examples of rvalue
s, as long as the return type is not a reference.
L-value to R-value
This type of conversion occurs in order to allow expressions to be assigned in a series of expressions, or as a result of situations where rvalues
are not present. Such as a function that returns a reference to a type.
For this implicit conversion scenario, the lvalue
is effectively copy-constructed into a temporary object so that it qualifies as an rvalue
type. Other potential conversion adjustments may be made as well such as removing the cv-qualifiers (const
and volatile
).
This is a fairly benign scenario of type decay, unless your lvalue
type has an extremely expensive copy-constructor.
Function to Pointer
The second scenario is another simple case. If the lvalue
is a function-type, not the actual expression of a function call, just the type, it can be implicitly converted to a pointer. This explains why you can assign a function to an expression that requires a function pointer, yet you are not required to use the &
to take the address of the function. Although if you do, you will still get the same results, because the implicit conversion no longer applies to the pointer to a function.
Array to Pointer Conversion
This is the case that I needed to understand in order to successfully add support for arrays to Alchemy. If an lvalue
is an array-type T
with a rank of N
, the lvalue
can be implicitly converted to a pointer to T
. This pointer refers to the first element in the original array.
I have been using C++ for almost two decades, and I am surprised that I did not discover this before now. Take a look at the following code. What will it print when compiled and run on a 64-bit system?
C++
void decaying(char value[24])
{
std::cout << "value contains " << sizeof (value) << "bytes\n";
}
Hopefully, you surmised that since T1
is open to the implicit conversion to a pointer to a char
, the sizeof
call will return the size of a 64-bit pointer. Therefore, this string
would be printed "T1 contains 8 bytes
".
I discovered this when I was building my Alchemy unit-tests to verify that the size of an array data type was properly calculated from a TypeList
definition. It only took a little bit of research for me to discover there is actually a special declaratory that can be used to force the compiler to prevent the implicit conversion of the array. Depending on your compiler and settings, you may get a helpful warning when this conversion is applied.
This declarator is called a noptr-declarator
. To invoke this declaratory, use a *
, &
or &&
in front of the name of the array. Parenthesis will also need to be placed around the operators and the name of the array. The resulting definition becomes a pointer or a reference to an array of type T
, rather than simply a pointer to T
. The sample below shows the declaration that is required to avoid the implicit cast.
C++
void preserving(char (&value)[24])
{
std::cout << "value contains " << sizeof (value) << "bytes\n";
}
Here is a brief example to demonstrate the syntax and differences:
int main(int argc, char* argv[])
{
char input[24];
std::cout << "input contains " << sizeof(input) << " bytes\n";
decaying(input);
preserving(input);
return 0;
}
Output
main: input contains 24 bytes
decaying: value contains 8 bytes
preserving: value contains 24 bytes
This simple modification allowed me to preserve the type information that I needed to properly process array data types in Alchemy. In my next entry, I will demonstrate how template specialization can be used to dismantle the array to determine the type and the rank (number of elements) that are part of its definition.
std::decay<T>
This function is part of the C++ Standard Library starting with C++ 14. It can be used to programmatically perform the implicit casts of type decay on a type. This function will also remove any cv-qualifiers (const
and volatile
). Basically, the original type T
will be stripped down to its basic type.
I haven't had a need to use this function in Alchemy. However, it is helpful to know about these utility functions and what is possible if I ever find the need to extract only the type.
Summary
The C++ compiler is a very powerful tool. Sometimes, it attempts to coerce types and data into similar forms in order to compile a program. In most cases, this is a very welcome feature because it allows for much simpler expressions and reduces clutter. However, there are some cases where the implicit casts can cause grief.
I stumbled upon the array to pointer type decay conversion during my development of Alchemy. Fortunately, there are ways for me to avoid this automatic conversion from occurring and I was able to work through this issue. Subtleties like this rarely appear during development. It is definitely nice to be aware that these behaviors exist, so you can determine how to work around them if you ever encounter one.