© 2015, 2016 Kyle Markley, kyle@arbyte.us
Open sourced under the BSD 3-Clause License
A typedef or a type alias gives an additional name to a type, but does not create a new type. These mechanisms are very useful to communicate the intent of a type - for example, std::string::size_type is not merely an integer, but is an integer that has something to do with a quantity of characters in a string. A typedef or type alias is also useful for maintenance, by creating a single place where a type may be changed if necessary.
Because a typedef or a type alias is simply an additional name for a type, you cannot prevent accidental mixing of variables that were intended to be treated as unlike types. For example, if you aliased int with the names "size_in_bits" and "size_in_bytes", your documented intent is clear, but the compiler will let you pass a size_in_bits value to a function expecting a size_in_bytes argument. It is also impossible to overload a function based on a parameter being size_in_bits or size_in_bytes. You might instead include "bits" or "bytes" in the name of the function, but the compiler will still let you pass the wrong kind of argument.
By contrast, an opaque typedef creates a new type, not merely an additional name for an old type. This provides all the advantages of the type system: overloading, no accidental mixing, and the potential for customization of the new type to define precisely how it should interoperate with other types.
There has been desire for opaque typedefs as a language feature for a long time and this has been the subject of many proposals (N1706, N1891, N2141, N3515, N3741, P0027R0), but it remains unclear if or when it will be adopted.
This library is the author's attempt to provide most of the value of opaque typedefs through a library, without waiting for opaque typedefs to become a language feature.
The following code, using this library, creates an opaque typedef of int. It has the same interface as an int (it can be added, shifted, incremented, compared, etc.) but the arguments and return values are of the newly-created type, not of int:
#include "opaque/numeric_typedef.hpp" struct myint : opaque::numeric_typedef<int, myint> { using base = opaque::numeric_typedef<int, myint>; using base::base; };
The library is conservative by default, permitting no implicit conversions. The new type may be explicitly (but not implicitly) constructed from the old type, and assignment from or to the old type requires an explicit conversion.
The library works by providing a wrapper for a single value of the old type, which is directly available via the public data member "value". The wrapping type mimics the interface of the old type and serves as a base class for the type that you create. Notice that for this example, the definition of the new type is empty except for inheriting the constructors of the wrapping type.
Because you are creating a new type, you have the opportunity to customize its interface by removing operations that are not desirable for your purposes, or by adding operations that are desirable. This can be done with very little code, and can turn semantic bugs into compile-time errors caught by the type system. (Adding implicit conversions may, of course, weaken the type safety.)
For example, if you were writing a microprocessor simulator, you might wish to have an address type interoperate with an offset type to perform addition and subtraction according to the following rules:
Arg1 Type | Arg2 Type | Result Type | ||
---|---|---|---|---|
address | + | address | → | error |
address | + | offset | → | address |
offset | + | address | → | address |
offset | + | offset | → | offset |
address | − | address | → | offset |
address | − | offset | → | address |
offset | − | address | → | error |
offset | − | offset | → | offset |
Furthermore, realizing that it does not make sense to multiply addresses, you would like to remove that operation. (Your type need not permit all the things an integer does merely because it is similar to an integer!)
The binary operators that have both a regular form and a compound assignment form (e.g. + and +=) have special treatment in this library, making it easy to specify the types involved in an operator of the regular form. All operators of the compound assignment form operator@= are provided by default. The related operators of the form operator@ are provided by opaque::numeric_typedef but not by opaque::numeric_typedef_base, making the latter better-suited for creating types with customized interfaces.
(It is possible to customize from opaque::numeric_typedef also, but removing an operator@ it provides requires a template specialization and perhaps a forward declaration of your type. It is therefore easier to use opaque::numeric_typedef_base.)
Using this library, the address and offset types described may be created like this (and note that they are templates in this example):
#include "opaque/numeric_typedef.hpp" template <typename T> struct offset : opaque::numeric_typedef<T, offset<T>> { using base = opaque::numeric_typedef<T, offset<T>>; using base::base; }; template <typename T> struct address : opaque::numeric_typedef_base<T, address<T>> , opaque::binop::addable <address<T>, true , address<T>, offset<T>> , opaque::binop::addable <address<T>, true , offset<T>, address<T>> , opaque::binop::subtractable<address<T>, false, address<T>, offset<T>> , opaque::binop::subtractable<offset<T> , false, address<T>, address<T>, T, T> { using base = opaque::numeric_typedef_base<T, address<T>>; using base::base; address& operator*=(const address&) = delete; address& operator+=(const address&) = delete; address& operator-=(const address&) = delete; address& operator+=(const offset<T>& o) { this->value += o.value; return *this; } address& operator-=(const offset<T>& o) { this->value -= o.value; return *this; } };
Here, offset is defined similarly to the previous example, but address is customized to remove operators *=, +=, and −= taking an address parameter (which were provided by default), and to add operators += and −= taking an offset parameter. Because address inherited from opaque::numeric_typedef_base, the operators + and − were not provided for address operands by default. The operators + and − are defined by inheritance from types in opaque::binop taking the following template arguments:
1. The return type
2. Whether the operation is commutative
3. The type of the left operand
4. The type of the right operand
5. The type to convert the left operand to
6. The type to convert the right operand to
When operator@ is not commutative, operator@= must be defined for the type of the left operand and accept a parameter (convertible to) the type of the right operand. When operator@ is commutative, the implementation may also consider an operator@= defined for the type of the right operand and accepting a parameter (convertible to) the type of the left operand. This permits optimization when only the right operand of operator@ is an rvalue, and also permits simpler definition of types - notice that offset did not need to define operator+= taking an address parameter.
In this example, the facility to convert the types of the operands was used for the case of subtracting two addresses to yield an offset. This was necessary because we removed operator−= accepting an address, but the implementation of opaque::binop::subtractable defines operator− in terms of operator−=, so it would try to use the removed function. Converting the operands to the original unwrapped type lets us define operator− successfully.
The library is header-only but distributed across several files. The only one you need to include is numeric_typedef.hpp, as shown in the tutorial.
The unit tests obviously must be compiled, and the build and test system for them is make. Yes, ordinary make, and either GNU make or BSD make is fine. Try "make check" to build and run the unit tests.
The makefiles respect ${CXX} as your compiler. You may edit compiler options in module.mk if necessary; the library ships with options appropriate for gcc and clang. The makefiles respect ${CXXFLAGS} if set on the make command line, but not via an environment variable.
On Mac, please include "NORMAL_LINK=" on your make command line. This clears NORMAL_LINK in order to remove the "-s" option, which strips symbols on Linux and BSD, but which causes problems on Mac (the linker claims it is ignored, but it is lying).
Note that this library is written in C++11, but benefits from the improved constexpr capabilities of C++14. Prefer C++14 (e.g. with "CXXFLAGS=-std=c++14") when you have the choice.
You may "make doc" to create documentation through Doxygen, but the output is fairly unhelpful where Doxygen is unable to understand the source code. Sections can be missed or misattributed. Hopefully, the tutorial and examples provided in the "examples" directory, plus the comments within numeric_typedef.hpp, are all you will need.
This library can trigger compiler bugs in gcc 4.8.1 related to member function overloads with ref-qualifiers, resulting in internal compiler errors. These problems can be worked around by removing the rvalue-qualified overloads and removing the lvalue-qualification from the remaining function. (There are four: the conversion operator in data.hpp and the unary +, −, and ~ operators in numeric_typedef.hpp)
opaque::numeric_typedef (opaque/numeric_typedef.hpp) is intended to be derived from to create simple numeric types.
opaque::numeric_typedef_base (opaque/numeric_typedef.hpp) is intended to be derived from to create numeric types with customized interfaces.
opaque::experimental::position_typedef (opaque/experimental/position_typedef.hpp) is a numeric typedef that factors out the operator+ and operator- behaviors described in the tutorial to make it easier to define types with those relationships. The tutorial showed address and offset types, but the same relationships are useful for time reference and time duration, etc. The fundamental idea is to model the relationship between a position and a distance.
opaque::inconvertibool (opaque/inconvertibool.hpp) is a boolean type that works like and with bool, but not with other types like int. This is a safer substitute for built-in bool if you are concerned about implicit conversions of bool that may be bugs. Inconvertibool gives you the implicit conversions you want and prevents the implicit conversions you don't.
opaque::experimental::safer_string_typedef (opaque/experimental/safer_string_typedef.hpp) is an opaque typedef for std::basic_string types (viz., std::string and such) but omits the functions that may modify the string or create a new one via const char *, because these are unsafe.
opaque::experimental::string_typedef (opaque/experimental/string_typedef.hpp) is similar but provides the full std::basic_string interface.
Walter Brown's N3741 is the most comprehensive proposal for opaque typedefs as a language feature. Although this library was not written for the purpose of emulating that proposal, it provides many of the features.
Section 3 of N3741 specifies that all primary and composite type traits should have the same value for an opaque-type and its underlying-type. In this library implementation, using an opaque typedef like "myint" from the tutorial above, the following primary traits mismatch:
Consequently, the following composite traits mismatch:
The language proposal states that overload resolution treats an argument of opaque type as a better match for a parameter of opaque type than of its underlying type. In this library implementation, the compiler does not know that the types are related, and an argument of opaque type cannot match an overload taking its underlying type except through an implicit conversion.
The language proposal states that the opaque type and its underlying type should be explicitly substitutible with no run-time cost (called a "type adjustment"). This library provides a public data member "value" of the underlying type, and using it is the moral equivalent of an explicit type adjustment.
Similarly, the proposal states that pointers and references to an opaque type should be explicitly convertible to pointers and references of the underlying type. This library implementation does not provide that feature, although a reinterpret_cast might work if your compiler implements the empty base class optimization.
Section 4 of N3741 discusses implicit type adjustment policies. This library models "private" by default, blocking implicit adjustments (and implicit conversions). The behaviors of "public" and "protected" can be provided by the user, but at the potential cost of type conversions, which could be costly if the underlying-type is a user-defined type.
Section 6 of N3741 (and later sections) show example syntax that depends on language changes. A library implementation cannot provide that syntax, but this library does strive to minimize the amount of code a user needs to write to define an opaque typedef.
Section 7 of N3741 discusses the specification of parameter and return types. By default, this library provides an interface taking the opaque type as parameter(s) and returns the opaque type as result. The implementation behind opaque::numeric_typedef provides special facilities to easily specify parameter and return types for binary operators operator@ that also have a compound assignment form operator@=. For other operations, customizations of parameter and return types can be provided by the user by providing appropriate overloads.
Section 8 of N3741 discusses opaque typedefs of class type and compiler-generated default trampoline functions. This is the chief advantage of opaque typedefs as a language feature. A library implementation of an opaque typedef must manually provide all of the trampoline functions in order to mimic the interface of the underlying type -- indeed, that is exactly what opaque::numeric_typedef does. In a library, that manual work must be duplicated for other types in order to use them as the underlying type of an opaque typedef.