Alias-declaration or type-alias with 'using' statement offers a more flexible way to define type aliases than typedef, mainly because of alias templates.
Defining type aliases or synonyms with typedef
has always been an indispensable part of writing a good quality C++ code. To outline how type aliases can help create more maintainable code, let's take an example of a simple banking application where a user can have multiple accounts. The following code declares two account types: Checking and Savings. An STL map collection is used to store the list of user accounts keyed by the int
user identifier:
class CheckingAccount;
class SavingsAccount;
// Map of UserId -> List of CheckingAccount
std::map<int, std::vector<CheckingAccount>> UserCheckingAccounts;
// Map of UserId -> List of SavingsAccount
std::map<int, std::vector<SavingsAccount>> UserSavingsAccounts;
One problem with the above code is that the user identifier type is hardcoded to int
. If the user identifier is changed to another type say std::string, it would require making changes all over. Also sometimes, working with the expansive STL declarations could cause a nervous system breakdown, e.g., when we have to declare them as function parameters:
void process(std::map<int, std::vector<SavingsAccount>>& userAccountsMap);
We can make this code easier to maintain by defining a type alias or synonym for the user identifier. Moreover, we can simplify this code even further by creating type aliases for STL collections:
// user identifier type alias
typedef int UserId;
// Define collection type aliases
typedef std::map<UserId, std::vector<CheckingAccount>> UserCheckingAccounts_t;
typedef std::map<UserId, std::vector<SavingsAccount>> UserSavingsAccounts_t;
Now, declaring a user-account-map collection with the synonyms is as easy as ABC:
// define maps
UserCheckingAccounts_t UserCheckingAccounts;
UserSavingsAccounts_t UserSavingsAccounts;
// type alias as function parameter
void process(UserSavingsAccounts_t& userAccountsMap);
The above code with type synonyms is more intuitive and self-documenting. For a large part, the typedef
declarations have worked well, but they have limitations, notably when it comes to working with templates. In the next sections, we would see what those limitations are and how they are overcome by the C++11 alias-declarations.
Since C++11, the using
statement can be used instead of typedef
to define type synonyms. This new method of defining type aliases is known as alias-declaration or type-alias. The synonyms of user identifier and account collection types with alias-declaration can be defined as follows:
using UserId = int;
using UserCheckingAccounts_t = std::map<UserId, std::vector<CheckingAccount>>;
using UserSavingsAccounts_t = std::map<UserId, std::vector<SavingsAccount>>;
Note that, the alias-declaration definitions look like variable declarations, which improves the readability. The improvement-in-readability argument is more persuasive when we compare the following function pointer alias definitions:
typedef void(*FP)(int);
// vs
using FP = void(*)(int);
// FP can be used to declare a function pointer
void foo(FP callback);
Like function pointer declaration, another exotic and a rather lesser-known declaration is of reference to an array. With array references, functions can take array parameters without decaying them to pointers. The below code shows an example of defining array-reference alias and using that as a function parameter. Note that how the array reference parameter allows us to use a range-based loop:
// create an alias of reference to int[4];
using IntArray4 = int(&)[4];
// or with typedef
// typedef int(&IntArray4)[4];
// Use the array reference alias as parameter type
void foo(IntArray4 array) {
// range-based loop on the array
for(int i : array) std::cout << i << " ";
}
// foo can be called on a int[4] as:
int arr[4] = {6,7,8,9};
foo(arr); // logs 6 7 8 9
Readability is required, but it might not be a compelling reason for some to ditch typedef
in favor of alias-declaration. However, alias-declarations have another feature, alias templates, which make them hard to overlook.
Unlike the typedef
, the alias-declarations can be templatized. With alias templates, we can define generic aliases and specialize them further:
// a template or generic alias
template<typename A>
using UserAccounts_t = std::map<UserId, std::vector<A>>;
using UserCheckingAccounts_t = UserAccounts_t<CheckingAccount>;
using UserSavingsAccounts_t = UserAccounts_t<SavingsAccount>;
By introducing a generic type alias, UserAccounts_t, we have made it easier to modify the code for a different STL collection later. We can change UserAccounts_t to another compatible collection without changing the code anywhere else:
// Change std::map to std::unordered_map
template<typename A>
using UserAccounts_t = std::unordered_map<UserId, std::vector<A>>;
To create UserAccounts_t generic alias with typedef
, we would have to wrap typedef
definition in a struct template:
template<typename A>
struct UserAccounts_t {
typedef std::map<UserId, std::vector<A>> type;
};
// and use it like:
UserAccounts_t<CheckingAccounts>::type UserCheckingAccounts;
As we can see, the typedef
version of the generic alias looks more like a hack than the cleaner alias template UserAccounts_t. Not only that, the alias-templates are easier to use in templates as they do not require typename
in front of them, whereas the struct wrapped typedef
must be preceded by typename
. For more coverage on alias-templates and how they are better than the struct nested typedef
, please refer "Effective Modern C++" by Scott Meyers.
When it comes to defining simpler type aliases, choosing between typedef
and alias-declaration could be a matter of personal choice. However, while defining the more complex template aliases, function-pointer aliases, and array reference aliases, the alias-declaration is a clear winner.
Type alias, alias template: cppreference
Aliases and typedefs: Microsoft docs
Difference between 'typedef' and 'using' in C++11: Stackoverflow