1. Changelog
1.1. Revision 2 - January 31st, 2022
-
Provide implementation of Transparent Aliases, available at this Clang fork.
-
Provide a proof of concept using the above implementation, which works on Windows, Mac, and Linux machines where the modified Clang compiler can be deployed.
-
Tweak redeclaration rules for Aliases, to solve Standard Library and other library redeclarations in the face of Aliases.
1.2. Revision 1 - September 15th, 2021
-
Adjust motivation / explanation of
and how it relates to this proposal.__attribute__ (( alias ()))
1.3. Revision 0 - May 15th, 2021
-
Initial release. ✨
2. Introduction & Motivation
After at least 3 papers were burned through attempting to solve the intmax_t problem, a number of issues were unearthed with each individual solution([N2465], [N2498], [N2425], [N2525]). Whether it was having to specifically lift the ban that §7.1.4 places on macros for standard library functions, or having to break the promise that
can keep expanding to fit larger integer types, the Committee and the community at large had a problem providing this functionality.
Thankfully, progress is being made. With Robert Seacord’s "Specific-width length modifier" paper was approved for C23 ([N2680]), we solved one of the primary issues faced with
improvements, which was that there was no way to print out a integral expression with a width greater than
. Seacord’s addition to the C standard also prevented a security issue that commonly came from printing incorrectly sized integers as well: there’s a direct correlation between the bits of the supported types and the bits of the given in the formatting string now, without having to worry about the type beyond a
check. This solved 1 of the 2 core problems.
2.1. Remaining Core Problem - Typedefs, ABI, and Macros
Library functions in a "very vanilla" implementation of a C Standard Library (e.g., simply a sequence of function declarations/definitions of the exact form given in the C Standard) have a strong tie between the name of the function (e.g.,
) and the symbol present in the final, compiled binary (e.g.,
). This symbol is tied to a specific numeric type (e.g.,
), which creates a strong relationship between the way the function is called (register usage, etc.) and more. Upgrading that type breaks old binaries that still call the old symbol; for example,
being handed a
instead of a
as an old application anticipates can result in the wrong registers being used, or worse. Thusly, because the Standard Library is bound by these rules and because implementations rely on functions with
-based types in them to resolve to a very specific symbol, we cannot upgrade any of the
s (e.g., what
is) or change anything about the functions (e.g., change
's declaration in any way consequential fashion).
Furthermore, macros cannot be used to "smooth" over the "real function call" because §7.1.4 specifically states that a user of the standard library has the right to deploy macro-suppressing techniques (e.g.,
) to call a library function (unless the call is designated to be a macro). This also includes preventing their existence as a whole with
: every call after that
directive to
must work and compile according to the Standard. While this guarantees users that they can always get a function pointer or a "real function name" from a given C Standard library function name, it also refuses implementations the ability to provide backwards compatibility shims using the only Standards-applicable tool in C++.
2.2. Liaison Issue - Stopping C++ Improvements
This is both a C Standard Library issue and a C++ Standard Library issue. Not only is it impossible to change the C Standard Library, but because of these restrictions and because the C Standard Library is included by-reference into C++, we cannot make any of the necessary changes to solve this problem in C++. This elevates the level of this problem to a liaison issue that must be fixed if we are to make forward progress in both C and C++.
2.3. Standardizing Existing Practice
While the C Standard Committee struggles with this issue, many other libraries that have binary interfaces communicated through shared or dynamically linked libraries have solved this problem. MSVC uses a complex versioning and symbol resolution scheme with its DLLs, which we will not (and could not) properly standardize here. But, other implementations have been using implementation-defined aliasing techniques that effectively change the symbol used in the final binary that is different from the "normal" symbol that would be produced by a given function declaration.
These techniques, expanded upon in the design section as to why we chose the syntax we did for this proposal, have existed for at least 15 years in the forms discussed before, and longer with linker-specific hacks.
2.4. C Issue - Prevented Evolution
Not fixing this issue also comes with a grave problem without considering C++ at all. We have no way of seamlessly upgrading our libraries without forcing end-users to consider ABI breakage inherit in changes type definitions or having library authors jump through implementation-specific and frightening hoops for creating (transparent) layers of indirection between a function call and the final binary. This means large swaths of C’s standard library, due to §7.1.4, are entirely static and non-upgradeable, even if we write functions that use type definitions that can change.
This is, by itself, a completely untenable situation that hampers the growth of C. If we cannot even change type definitions due to constraints such as linkage names from old code without needing a computer-splitting architectural change (e.g., the change from
"32-bit" architectures to
"64-bit" architectures that allowed for
to change), with what hope could be possibly have in getting C to evolve? How can we have it meet current hardware specifications and software needs? Users have been raging on about the lack of an
in C, or a maximum integer type, and some implementers and platform users have stated:
I am unreasonably angry about this, because the
situation has kept me from enabling completely-working first-class
intmax_t support out of clang for a decade. In that decade, I personally would have used 128b literals more times in personal and professional projects than the entire world has used intmax_t to beneficial effect, ever, in total.
int128
At the surface of this issue and as illustrated by the many failed — and one successful — papers for
is that we need a better way for type definitions to be used for interfaces in the C standard. Underlying it is a wound that has begun to fester in the presence of not having a reason to invent wildly new architectures that necessitate fundamentally recompiling the world. Our inability to present a stable interface for users in a separable and Standards-Compliant way from the binary representation of a function that we cherish so deeply is becoming an increasing liability. If every function (the fundamental unit of doing work) essentially becomes impossible to change in any way, shape, or form, then what we are curating is not a living and extensible programming language but a dying system that is unequivocally doomed to general failure and eventual replacement.
3. Design
Our goal with this feature is to create a no-cost, zero-overhead function abstraction layer that prevents a type definition or other structure from leaking into a binary in a permanent and non-upgradable fashion. From the motivation and analysis above, we need the following properties:
-
It must be a concrete name, not a macro.
-
It must be able to decay to a function pointer when referenced by name, like a normal function declaration, and that value must be usable.
-
It should not require producing a symbol on non-interpreter implementations of C.
-
It should allow for an implementation to upgrade or change the arguments or return type of a concrete symbol without requiring a detectable binary break on any binary C implementation.
To fulfill these requirements, we propose the a new transparent-alias construct that, in general, would be used like such:
extern long long __glibc_imaxabs228 ( long long ); extern __int128_t __glibc_imaxabs229 ( __int128_t ); /* ... */ #if __GNU_LIBC <= 228 _Alias imaxabs = __glibc_imaxabs228 ; #else _Alias imaxabs = __glibc_imaxabs229 ; #endif /* ... */ int main () { intmax_t x = imaxabs ( -50 ); return ( int ) x ; }
It is composed of the
keyword, followed by an identifier, the
s token, and then another identifier. The identifier on the right hand side must be either a previously declared transparent-alias or name a function declaration. Below, we explore the merits of this design and its origins.
3.1. Transparency - "Type Definitions, but for Functions"
We call this transparent because it is, effectively, unobservable from the position of a library consumer, that this mechanism has been deployed. The following code snippet illustrates the properties associated with Transparent Function Aliases:
#include <assert.h>int other_func ( double d , int i ) { return ( int )( d + i ) + 1 ; } int real_func ( double d , int i ) { return ( int )( d + i ); } _Alias alias_func = real_func ; /* The below is a Constration Violation. You cannot redeclare */ /* a function alias with an incompatible signature. */ //void alias_func(void); /* The is fine. You can redeclare a transparent alias, so long as */ /* the redeclaration is of compatible type. */ /* This, in particular, solves the Standard Library’s issue with */ /* §7.1.4’s permission to redeclare (Standard) Library symbols. */ void alias_func ( double d , int i ); /* No Constraint Violation: redeclaration of an alias pointing /* to the same declaration is fine. */ _Alias alias_func = real_func ; /* Constraint Violation: redeclaration of an alias pointing */ /* to a different declaration than the first one is not */ /* allowed. */ //_Alias alias_func = other_func; int main ([[ maybe_unused ]] int argc , [[ maybe_unused ]] char * argv []) { assert ( & alias_func == & real_func ); // no Constraint Violation typedef int ( real_func_t )( double , int ); real_func_t * real_func_ptr = alias_func ; // decays to function pointer of real_func real_func_t * real_func_ptr2 = & alias_func ; // function pointer to real_func [[ maybe_unused ]] int is_3 = alias_func ( 2.0 , 1 ); // invokes real_func directly [[ maybe_unused ]] int is_4 = real_func_ptr ( 3.0 , 1 ); // invokes real_func [[ maybe_unused ]] int is_5 = real_func_ptr2 ( 3.0 , 2 ); // invokes real_func assert ( is_3 == 3 ); // no constraint violation assert ( is_4 == 4 ); // no constraint violation assert ( is_5 == 5 ); // no constraint violation assert ( real_func_ptr == & real_func ); // no constraint violation assert ( real_func_ptr == & alias_func ); // no constraint violation assert ( real_func_ptr2 == & real_func ); // no constraint violation assert ( real_func_ptr2 == & alias_func ); // no constraint violation return 0 ; }
The notable properties are:
-
always "forwards" its calls toalias_func
without needing the end-user to call "real_func
" directly;real_func -
can be used in constant expressions, just like normal functions with their address taken;alias_func -
, like any other function call, cannot be redeclared as a normal function declaration of any form;alias_func -
cannot be re-aliased to a different function call after the first;alias_func -
any function pointer obtained from
is identical to a function pointer obtainedreal_func
; and,alias_func -
andreal_func
have identical addresses.alias_func
In short,
works like any other function declaration would, but is not allowed to have its own function definition. It is simply an "alias" to an existing function at the language level. Given these properties, no implementation would need to emit a whole new function address for the given type; any binary-producing implementation would produce the same code whether the function was called through
or
. It gets around the requirement of not being able to define C functions as macros, while maintaining all the desirable properties of a real C function declaration.
It also serves as a layer of indirection to the "real function", which means function alias definitions and type definitions can be upgraded with one another while improving backwards compatibility.
3.2. Inspiration: Existing Practice
It is not a coincidence in the initial example that we are using
prefixes for the 2
function calls. Tying a function to a name it does not normally "mangle" to in the linker is a common implementation technique among more advanced C Standard Library Implementations, such as musl-libc and glibc. It is also a common technique deployed in many allocators to override symbols found in downstream binary artefacts, albeit this proposal does not cover the "weak symbol" portion of the alias techniques deployed by these libraries since that is sometimes limited to specific link-time configurations, binary artefact distributions, and platform architecture.
Particularly, this proposal is focusing on the existing GCC-style attribute and a Clang-style attribute. The GCC attribute ([gcc-attribute]) follows this proposal fairly closely (but not exactly) in its requirements, by effectively allowing for an existing function declaration to have its address made identical to the function it is aliasing:
void __real_function ( void ) { /* real work here... */ ; } void function_decl ( void ) __attribute__ (( alias ( "__real_function" )));
This code will set up
to match the address of
. It is common implementation practice amongst compilers that are GCC-compatible and that focus on binary size reduction and macro-less, transparent, zero-cost indirection. It has the small caveat that it requires the target of the attribute to be fully defined before this happens, so it can definitively insert the address. For example, here is documentation from the Keil’s armcc compiler ([keil-attribute]):
static int oldname ( int x , int y ) { return x + y ; } static int newname ( int x , int y ) __attribute__ (( alias ( "oldname" ))); int caller ( int x , int y ) { return oldname ( x , y ) + newname ( x , y ); } This code compiles to:
AREA || . text || , CODE , READONLY , ALIGN = 2 newname ; Alternate entry point oldname PROC MOV r2 , r0 ADD r0 , r2 , r1 BX lr ENDP caller PROC PUSH { r4 , r5 , lr } MOV r3 , r0 MOV r4 , r1 MOV r1 , r4 MOV r0 , r3 BL oldname MOV r5 , r0 MOV r1 , r4 MOV r0 , r3 BL oldname ADD r0 , r0 , r5 POP { r4 , r5 , pc } ENDP
There are many compilers which implement exactly this behavior with exactly this GCC-extension syntax such as Oracle’s C Compiler, the Intel C Compiler, the Tiny C Compiler, and Clang ([oracle-attribute], [intel-attribute]). Clang also features its own
-style attribute, where the function’s name is "mangled" to exactly the name given ([clang-attribute]). Microsoft Visual C uses a (slightly more complex) stateful pragma mechanisms and external compiler markup ([msvc-attribute]).
Intermediate object files may still carry the name "
", but the binary in most binary-artefact producing implementations will not and only the target name will appear. This very closely matches the desired behavior: we want to create an entity that, ultimately, has no linkage and is specified in such a way that it will almost always disappear in both intermediate objects and final artefacts, wherever possible, while still providing a level of compile-time indirection. In this way this feature departs from how
in GCC’s attribute syntax works, but we find this departure to be worth the effort.
3.2.1. Other Existing Practice: asm (...)
and #pragma
There are 3 other proofs of practice in the industry today. One was an MSVC-like
behavior/
-file specification. Another was a Clang-like
-attribute that behaved similarly to rename behavior. The third was a
that simply made the linker do a find-and-replace symbol swap. When evaluated as potential alternatives to the syntaxes chosen here, there were a number of deficiencies for providing backwards compatibility. Notably, there are 2 chief concerns at play:
-
the function entity/entities the end-user must interact with from a given library; and,
-
valid interpretations of the directive in a world where the implementation does not produce binary artefacts.
Clang’s
, MSVC’s
-based approach, and Oracle’s
([oracle-pragma]), are harder to standardize because each mechanism relies too heavily on the linker and the details of binary artefacts. The GCC-style attribute is tied to a front-end entity and, therefore, abstracts away binary changes as a means left to the implementation, Clang and MSVC’s approaches are not tied to any entity that exists in the program in general. Export
s and
attributes can be used to reference any symbol, by unchecked string, that can be resolved at any later stage of compilation (possibly during linking and code generation). There is no good way to standardize such behavior because there are no meaningful semantic constraints that can be placed on their designs that are enforceable within the boundaries of the Abstract Machine.
Contrast this to GCC’s attribute. It requires that a previous, in-language definition exists. If that definition does not exist, the program has a constraint violation:
#if 0 extern inline int foo () { return 1; } #endif int bar () __attribute__ (( alias ( "foo" ))); // <source>:5:27: error: alias must point to a defined variable or function // int bar () __attribute__((alias("foo"))); // ^ int main () { return bar (); }
GCC’s design is more suitable for Standard C, since we do not want to specify this in terms of effects on a binary artefact or "symbols". GCC’s design makes no imposition on what may or may not happen to the exported symbols, only ties one entity to another in what is typically known as an implementation’s "front end". Whether or not final binary artefacts do the right thing is still up to an implementation (and always will be, because the Standard cannot specify such). This gives us proper semantics without undue burden on either specification or implementation.
Unfortunately, GCC’s definition of an alias requires that there exists a definition. We see no reason to keep that constraint as present, and therefore remove that constraint in the design of this feature.
3.2.2. Implementation Experience
An implementation of Transparent Aliases is available at this Clang fork. It provides the feature and, when built and installed, can be used to run this suite of tests.
The test suite sets up a supposed ABI on both Linux, Mac, and Windows by creating a DLL with exported functions. Without redefining the functions, the tests show that taking an old Shared Object or a Windows DLL, upgrading it and doing an in-place replacement of that file with a newly built object that contains new definitions, still lets old applications work while new applications return the correct and proper functionality from the updated code.
The test suite also outputs a version number while doing a comparison test. A negative, custom-defined
number - which uses bits in all available registers on 2s complement 32-bit and 64-bit systems - is properly returned from a function
that computes its absolute value. That value is further compared equal to the size of a defined
, which resolves to
in the old application (
) and
in the new application (
):
#include <my_libc/maxabs.h>#include <stdio.h>int main () { intmax_t abi_is_hard = - ( intmax_t ) sizeof ( intmax_t ); intmax_t but_not_that_hard = maxabs ( abi_is_hard ); printf ( "%d \n " , my_libc_magic_number ()); return (( int )( but_not_that_hard ) == sizeof ( intmax_t )) ? 0 : 1 ; }
The program only returns
(and passes the test suite) if the returned value is properly negated and compares equal to the original size. The test output on all 3 of Linux (Ubuntu and Debian), Mac (Mojave), and Windows (Version 10 and 11) is as follows:
[ proc] Executing command: ctest -j10 -C Debug -T test --output-on-failure[ ctest] Cannot find file: transparent-aliases/.cmake/build/DartConfiguration.tcl[ ctest] Site:[ ctest] Build name:( empty) [ ctest] Cannot find file: transparent-aliases/.cmake/build/DartConfiguration.tcl[ ctest] Test project transparent-aliases/.cmake/build[ ctest] Start6 : discard_warning.compile.clean[ ctest] Start10 : app_old.lib_new-copy[ ctest] Start8 : app_old.lib_old[ ctest] Start9 : app_new.lib_new[ ctest] Start3 : example3[ ctest] Start5 : discard_warning[ ctest] Start4 : example4[ ctest] Start2 : example2[ ctest] Start1 : example1[ ctest] 1 /11 Test#6: discard_warning.compile.clean .... Passed 0.20 sec [ ctest] Start7 : discard_warning.compile[ ctest] 2 /11 Test#5: discard_warning .................. Passed 0.12 sec [ ctest] 3 /11 Test#10: app_old.lib_new-copy ............. Passed 0.20 sec [ ctest] Start11 : app_old.lib_new[ ctest] 4 /11 Test#4: example4 ......................... Passed 0.12 sec [ ctest] 5 /11 Test#2: example2 ......................... Passed 0.09 sec [ ctest] 6 /11 Test#1: example1 ......................... Passed 0.06 sec [ ctest] 7 /11 Test#8: app_old.lib_old .................. Passed 0.22 sec [ ctest] 8 /11 Test#9: app_new.lib_new .................. Passed 0.19 sec [ ctest] 9 /11 Test#3: example3 ......................... Passed 0.16 sec [ ctest] 10 /11 Test#11: app_old.lib_new .................. Passed 0.03 sec [ ctest] 11 /11 Test#7: discard_warning.compile .......... Passed 5.56 sec [ ctest] [ ctest] 100 % tests passed,0 tests failed out of11 [ ctest] [ ctest] Total Test time( real) = 5 .79 sec[ ctest] CTest finished withreturn code0
We specifically make sure to turn off typical automatic RPATH handling on *Nix machines, and make sure they load the DLL present in
(so that simply copying over the application-local DLL ensures it is loading the DLL associated with the test.
This serves as concrete evidence that existing implementations can use the feature to produce the same point. Users using the test compiler could reproduce these results successfully.
3.3. Why not an [[ attribute ]]
?
At this point in time, one might wonder why we do not propose an attribute or similar for the task here. After all, almost all prior art uses an attribute-like or literal
syntax. Our reasons are 2-fold.
3.3.1. Standard Attributes may be ignored.
The ability to ignore an attribute and still have a conforming program is disastrous for this feature. It reduces portability of libraries that want to defend against binary breakages. Note that this is, effectively, the situation we are in now: compilers effectively ruin any implementation-defined extension by simply refusing to support that extension or coming up with one of their own. Somewhat ironically, those same vendors will attend C Committee meetings and complain about binary breakages. We then do not change anything related to that feature area, due to the potential of binary breakages.
The cycle continues and will continue ad nauseum until Standard C provides a common-ground solution.
3.3.2. There is no such thing as "mangled name" or "symbol name" in the Standard.
Any attempt at producing normative text for a "symbol name" construct is incredibly fraught with peril and danger. Vendors deserve to have implementation freedom with respect to their what their implementation produces (or not). Solving this problem must be done without needing to draft specification for what a "binary artefact" or similar may be and how an attribute or attribute-like construct could affect it. If this feature relies primarily on non-normative encouragement or notes to provide ABI protection, then it is not fit for purpose.
Therefore, we realize that the best way to achieve this is to effectively allow for a transparent aliasing technique for functions, similar to type definitions. It must be in the language and it must be Standard, otherwise we can never upgrade any of our type definitions without waiting for an enormous architectural break (like the 32-bit to 64-bit transition).
3.4. Backwards-Compatibility with "Vanilla" C Standard Libraries
One of the driving goals behind this proposal is the ability to allow "vanilla" C Standard Library Implementations to use Standards-only techniques to provide the functions for their end-user. Let us consider an implementation — named
, that maybe produces a
binary — that, up until today, has been shipping a
function declaration for the last 2 decades. Using this feature, we can provide an _entirely backwards compatible_, binary-preserving upgraded implementation of
that decides to change it’s
function declarations. For example, it can use 2 translation units
and
and one header,
, to produce a conforming Standard Library implementation that is also backwards-compatible with binaries that continue to link against
:
:
#include <inttypes.h>__int128_t __imaxabs_vanilla_v2 ( __int128_t __value ) { if ( __value < 0 ) return - __value ; return __value ; }
:
extern inline long long imaxabs ( long long __value ) { if ( __value < 0 ) return - __value ; return __value ; }
:
/* upgraded from long long in v2 */ typedef __int128_t intmax_t ; extern intmax_t __imaxabs_vanilla_v2 ( intmax_t ); _Alias imaxabs = __imaxabs_vanilla_v2 ;
As long as
is linked with the final binary artefact
, the presumed mangled symbol
will always be there. Meanwhile, the "standard"
will have the normal
symbol that is tied in a transparent way to the "Version 2" of the vanilla implementation,
. This produces a perfectly backwards compatible interface for the previous users of
. It allows typedefs to be seamlessly upgraded, without breaking already-compiled end user code. Newly compiled code will directly reference the v2 functions with no performance loss or startup switching, getting an upgraded
. Older programs compiled with the old
continue to reference old symbols left by compatibility translation units in the code.
This means that C Standard Libraries will have a language-capable medium of upgrading their code in a systemic and useful fashion. A working, tested, compiled version of this example is available at []().
3.4.1. Standard Library Redeclaration
One of the bigger problems that comes with this design space is that the C Standard Library specifically allows for a user to redeclare an existing standard library symbol:
Provided that a library function can be declared without reference to any type defined in a header, it is also permissible to declare the function and use it without including its associated header.
That means the following declaration in this translation unit is legal:
extern char * strcpy ( char * restrict s1 , const char * restrict s2 ); int main () { return 0 ; }
This is a restriction that is special to C. Thankfully, we are not particularly concerned about the ability to upgrade this function: users who are declaring Standard Library functions without including the header like this are doing this strictly as experts. They have a strong expectation of what symbol they are getting from their distribution. Transparent aliases are meant to be used for functions which rely on type definitions or structures defined in the standard library, including (but not limited to):
-
(struct timespec
, and other functions)timespec_get -
(time_t
,time
,mktime
, and more functions)difftime -
(intmax_t
,imaxabs
, and similar functions)strtoimax -
(wchar_t
,mbstowcs
, and similar functions)wcslen -
and so on, and so forth.
These are types which are controlled exclusively by the standard library and are known to change on 32-bit and 64-bit systems as well as transition based on locale and other compile-time settings. A single DLL can be distributed on a system and accommodate a variety of preprocessor-based switches or other compile-time information and allow an information to accommodate and/or upgrade a given system’s symbol table (or DLL/SO’s symbol table) without breaking old, original code.
Nevertheless, it is a common practice to redeclare standard library symbols after they are defined by a header. While we do not touch the strong/weak declaration portion of previous existing practice, we do provide wording which allows for a redeclaration of an
as a function declaration. This allows for a common use case in code, even if the standard does not explicitly support redeclaring e.g.
or
.
3.5. The _Alias a = b
syntax
The primary reason the syntax
is chosen here is because we want to provide an in-language construct for doing this without requiring that the end-user completely re-declare the function they want to alias. For example, an alternative syntax was considered as follows:
extern void b ( int w , double x , struct yy * y , struct zz * z ); void a ( int w , double x , struct yy * y , struct zz * z ) = b ;
This gave the "function" and "redeclaration" feeling to
, but it required that all arguments essentially be reproduced exactly (or risk constraint violations). This introduces fault-intolerance, where function arguments could change and cause breakage in downstream code. This could result in a lot of unnecessary maintenance work for end-users and package maintainers alike responding to library developers and their change in type definitions or similar. It might end up tying future developer’s hands not for binary stability reasons, but for source breakage reasons. While source breaks are preferred over binary breaks, we want to avoid this being a problem altogether. This is an improvement over the GCC alias attribute, where two identical function declarations, differing only in the name, were required.
3.6. The literal word _Alias
is the safe choice. Originally, we used the word
as there was very little option to create a keyword that more appropriately mirrors "
but for functions".
is already a prominent identifier in C codebases, and reusing
is not a very good idea for something that does not declare a type.
C++ took the keyword
, and so far it seems to have made most C and C++ developers stay away from the keyword altogether. Nevertheless, the wording uses a stand-in
. The suggestions we have for the token are as follows, based on not being findable in publicly available codebase sets (either on isocpp.org's code search of package manager code for Linux Distributions, GitHub’s dataset for code, and similar sources):
-
(made safer by C++ using it as a keyword)using -
(long, pretty good name)using_alternate -
(long, pretty good name)alternate_alias -
, with a_Alias
header and a< stdalias . h >
in it (trying to avoid the underscore-capital keywords since some folk do not appreciate it)#define alias _Alias -
(long enough it conflicts with no developers, recommended by Godbolt himself)sameysameynamename
Various names were also thought of and unfortunately discarded because they exist as macro names and identifier names in publicly available code today:
-
using_name -
/decl_alias declalias -
/alias_decl aliasdecl -
/alias_def aliasdef -
/name_decl namedecl -
/name_def namedef -
/name_alias namealias -
function_alias -
/func_def funcdef -
/func_decl funcdecl
While we use the word
right now as a stand-in, we would appreciate feedback on what name to pick.
4. Future Directions
There has been expressed want for transparent aliases for non-functions. That is:
int f ( int value ) { return y + y ; } int main () { int x = 1 ; _Alias y = x ; return & y == & x ? f ( y ) : 0 ; // returns f(x) == 2 }
We think this would be a useful general purpose compile-time renaming mechanism. But, that is beyond the scope of this paper at the moment. Various implementation-specific attributes above work with variables but we would like more time to work on the semantics of such before attempting to standardize such a feature. The precedence and usage experience for non-variables is far more important. As it is a constraint violation to do this with functions (and not undefined behavior), we have room to do this in the future.
There were also further suggestions to have the ability to make "
" aliases: that is, the ability to be able to define a compile-time alias, but be able to redeclare over it and completely erase the old declaration in a way that can be replaced, at compile-time, with a new (and potentially incompatible) declaration. This is actually implemented in the current implementation, and it works as expected, but it is not provided in this paper. There is, thankfully, plenty of room in the grammar to allow for this.
5. Wording
The following wording is registered against [N2596].
5.1. Modify "§6.2.1 Scopes of identifiers", paragraph 1
An identifier can denote an object; a function; a tag or a member of a structure, union, or enumeration; a typedef name; a transparent alias name; a label name; a macro name; or a macro parameter.
5.2. Modify "§6.2.1 Scopes of identifiers", paragraph 4
Change every instance of "
declarator or type specifier
" to be "
declarator, transparent alias, or type specifier
".
5.3. Modify "§6.2.1 Scopes of identifiers", paragraph 7
Structure, union, and enumeration tags have scope that begins just after the appearance of the tag in a type specifier that declares the tag. Each enumeration constant has scope that begins just after the appearance of its defining enumerator in an enumerator list. A transparent alias name has a scope that begins after the appearance of the transparent alias target in its definition. Any other identifier has scope that begins just after the completion of its declarator.
5.4. Modify paragraph 6 "§6.2.2 Linkages of identifiers"
The following identifiers have no linkage: an identifier declared to be anything other than an object or a function; a transparent alias; an identifier declared to be a function parameter; a block scope identifier for an object declared without the storage-class specifier extern.
5.5. Add transparent aliases to "§6.2.3 Name spaces of identifiers", paragraph 1, last bullet
…
- all other identifiers, called ordinary identifiers (declared in ordinary declarators , transparent aliases , or as enumeration constants).
5.6. Add a new keyword to "§6.4.1 Keywords", Syntax, paragraph 1
keyword: one of
…
ALIAS-TOKEN
5.7. Modify "§6.7 Declarations" as follows...
5.7.1. §6.7 Syntax, paragraph 1, with a new "declaration" production
- declaration:
- declaration-specifiers init-declarator-listopt ;
- attribute-specifier-sequence declaration-specifiers init-declarator-list ;
- static_assert-declaration
- attribute-declaration
- transparent-alias-declaration
5.7.2. §6.7 Constraints, paragraphs 2 and 3
A declaration other than a static_assert or attribute declaration shall declare at least a declarator (other than the parameters of a function or the members of a structure or union), a tag , a transparent alias , or the members of an enumeration.If an identifier has no linkage, there shall be no more than one declaration of the identifier (in a declarator or type specifier) with the same scope and in the same name space, except that:
- a typedef name may be redefined to denote the same type as it currently does, provided that type is not a variably modified type;
- a transparent alias name may be redefined as specified in 6.7.12; and
- tags may be redeclared as specified in 6.7.2.3.
5.7.3. §6.7 Semantics, paragraph 5
A declaration specifies the interpretation and properties of a set of identifiers. A definition of an identifier is a declaration for that identifier that:
for an object, causes storage to be reserved for that object;
for a function, includes the function body;129)
for an enumeration constant, is the (only) declaration of the identifier;
for a typedef name, is the first (or only) declaration of the identifier
.; or- for a transparent alias name, is the first (or only) declaration of the identifier.
5.8. Modify "§6.9 External definitions" paragraphs 3 and 5
There shall be no more than one external definition for each identifier declared with internal linkage in a translation unit. Moreover, if an identifier declared with internal linkage is used directly or indirectly (e.g., through a transparent alias) in an expression (other than as a part of the operand of a sizeof or _Alignof operator whose result is an integer constant), there shall be exactly one external definition for the identifier in the translation unit.…
An external definition is an external declaration that is also a definition of a function (other than an inline definition) or an object. If an identifier declared with external linkage is used directly or indirectly (e.g., through a transparent alias) in an expression (other than as part of the operand of a sizeof or _Alignof operator whose result is an integer constant), somewhere in the entire program there shall be exactly one external definition for the identifier; otherwise, there shall be no more than one.176)
5.9. Add a new sub-clause "§6.7.12 Transparent alias"
6.7.12 Transparent aliasSyntax
- transparent-alias-declaration:
attribute-specifier-sequenceopt ALIAS-TOKEN identifier attribute-specifier-sequenceopt = identifier ; Let the identifier on the left hand side be the transparent alias name and the identifier on the right hand side be the transparent alias target.Constraints
A transparent alias target must refer to a preceding and visible transparent alias or a preceding and visible function declaration. A transparent alias being redefined shall refer to the same function declaration1⭐⭐. A transparent alias being redeclared as a function declaration shall still be considered a transparent alias after the redeclaration. The redeclaration shall still refer to the transparent alias target when called, and the function declaration shall have a compatible type with the transparent alias target.Semantics
A transparent alias refers to an existing function declaration, either directly or through another transparent alias. A transparent alias does not produce a new function declaration; it is only a synonym for the transparent alias target specified. If the transparent alias target is another transparent alias, it is translated, recursively, until the existing function declaration is determined.A transparent alias that refers to a function is a function designator (6.3.2.1). If its address is taken with the unary address operator (6.5.3.2) or used in an expression, it is converted to a pointer-to-function whose address is identical to the function declaration that the transparent alias target refers.The optional attribute specifier sequence after the transparent alias name appertains to the transparent alias name.EXAMPLE 1 The following program contains no constraint violations and does not call
:
abort #include <assert.h>void do_work ( void ); void take_nap ( void ); ALIAS - TOKEN work_alias = do_work ; ALIAS - TOKEN nap_alias = take_nap ; ALIAS - TOKEN alias_of_work_alias = work_alias ; ALIAS - TOKEN alias_of_nap_alias = nap_alias ; int main () { assert ( & do_work == & work_alias ); assert ( & do_work == & alias_of_work_alias ); assert ( & work_alias == & alias_of_work_alias ); assert ( & take_nap == & nap_alias ); assert ( & take_nap == & alias_of_nap_alias ); assert ( & nap_alias == & alias_of_nap_alias ); assert ( & take_nap != & work_alias ); assert ( & do_work != & alias_of_nap_alias ); ALIAS - TOKEN local_work_alias = alias_of_work_alias ; assert ( & local_work_alias == & alias_of_work_alias ); do_work (); work_alias (); // calls do_work alias_of_work_alias (); // calls do_work local_work_alias (); // calls do_work take_nap (); nap_alias (); // calls take_nap alias_of_nap_alias (); // calls take_nap return 0 ; } EXAMPLE 2 Valid redeclarations:
int zzz ( int requested_sleep_time ); ALIAS - TOKEN sleep_alias = zzz ; ALIAS - TOKEN sleep_alias = sleep_alias ; ALIAS - TOKEN sleep_alias_alias = zzz ; ALIAS - TOKEN sleep_alias = sleep_alias_alias ; void func ( void ); int main () { // Inner scope: no constraint violation _Alias func = func ; } EXAMPLE 3 Invalid redeclarations:int zzz ( int requested_sleep_time ); int truncated_zzz ( int requested_sleep_time ); ALIAS - TOKEN sleep_alias = sleep_alias ; // constraint violation: sleep_alias does // not exist until the // semicolon is reached ALIAS - TOKEN zzz = truncated_zzz ; // constraint violation: cannot hide // existing declaration ALIAS - TOKEN truncated_zzz = truncated_zzz ; // constraint violation: cannot change // function declaration // to transparent alias ALIAS - TOKEN valid_sleep_alias = zzz ; double valid_sleep_alias ( double requested_sleep_time ); // constraint violation: // redeclaring a // transparent alias with // non-compatible type EXAMPLE 4 An alias can be redeclared as either an alias or a function declaration so long as it is compatible:
double purr ( void ) { return 1.0 ; } ALIAS - TOKEN meow = purr ; double meow ( void ); // compatible redeclaration, still calls "purr" ALIAS - TOKEN meow = purr ; // compatible redeclaration int main () { double x = meow (); // calls purr return ( int )( v ); // returns 1 } EXAMPLE 5 Compatible and completed types through aliases:
void otter ( int ( * )[]); _Alias water_noodle = otter ; void otter ( int ( * )[ 2 ]); // water_noodle has type void (int (*)[2]) EXAMPLE 6 Shadowing and compatible types through aliases:
void cookie ( int ( * )[ 2 ]); int main () { _Alias biscuit = cookie ; { int cookie ; // Shadow outer declaration. { void cookie ( int ( * )[]); // biscuit has type void (int (*)[2]) // due to shadowing } } return 0 ; } EXAMPLE 7 Composite and compatible types through aliases:
void otter ( int ( * )[], int ( * )[ 2 ]); int main () { _Alias water_sausage = otter ; { void otter ( int ( * )[ 2 ], int ( * )[]); // water_sausage and otter have // composite type void (int (*)[2], int (*)[2]) } } 1⭐⭐) If the transparent alias target points to another transparent alias, then the alias target is first resolved. The resolution occurs recursively until a function declaration is the alias target. Equality between two alias names determines whether or not they ultimately refer to the same function declaration. Resolution of a transparent alias target happens before the synonym is declared or redeclared, meaning a transparent alias name may refer to itself when it is being redeclared, but not when it is first declared.Recommended Practice
Implementations and programs may use aliases as a way to produce stability for translation units which rely on specific function declaration and definitions being present while aliasing a common declaration name for a more suitable interface. It may be particularly helpful for function declarations which use type definitions (6.7.8) in return and parameter types. Programs may update and upgrade alias definitions alongside type definitions to preserve entities and symbols in a given program while letting newer programs take advantage of upgraded functionality.EXAMPLE 8 Versioning of a function call
while keeping old externally-defined function definitions available within the program.
imaxabs extern intmax_t __imaxabs_32ish ( __int32 value ); extern intmax_t __imaxabs_64ish ( __int64 value ); extern intmax_t __imaxabs_128ish ( __int128 value ); #if VER0 typedef int intmax_t ; ALIAS - TOKEN imaxabs = __imaxabs_32ish ; #elif VER1 typedef long long intmax_t ; ALIAS - TOKEN imaxabs = __imaxabs_64ish ; #elif VER2 typedef __int128_t intmax_t ; ALIAS - TOKEN imaxabs = __imaxabs_128ish ; #endif