Click here to Skip to main content
15,389,719 members
Articles / Programming Languages / C++
Article
Posted 22 Aug 2021

Stats

5.2K views
43 downloads
3 bookmarked

Wild West Coding: MACROs

Rate me:
Please Sign up or sign in to vote.
4.73/5 (3 votes)
22 Aug 2021CPOL6 min read
An alternative solution to a standard macro that aims to make macros, just a little bit less evil
This article aims to address naming conflicts and readability to the common macro by introducing an alternative. A dispatch macro, that serves as a portability wrapper, to be known as a characteristic.

Introduction

Macros are based on the #define directive, which specifies a macro, comprising an identifier name and a string or numerics, to be substituted by the preprocessor for each occurrence of that macro in the source code. Macros are commonly used for abstracting pragmas, declspecs, attributes, prerequisites, and feature checks.

Many programmers believe know that macros are evil. One of the major reasons for this is that they pollute the code base as they take over anything of the same name. That having been said, they are often, the only way to access compiler specific features in a cross platform manner. Macros have a delicate line to walk between two things. The first being unique naming. This goes to the code base pollution as macros are global. They are normally defined just before first use, and undefined directly after last use. This helps to offset the pollution. The second concern is making the name meaningful. The combination of the two leads to results that are often less than ideal. As a substitute, we will offer a portability wrapper, using attribute-style naming, that aims to solve this conflict.

Background

While working on a code base challenge introduced by a video on CppCon, I thought of a few alternatives to some of the more controversial subjects in programming. These ideas, that began purely academic, seemed as though they would have a wide array of actual uses. Although some of these alternatives required library structures to back them, which also would need explaining. An idea began to form. A series of articles that aims to tackle controversial concepts in coding and provide alternative means and methods. The first of which will be the most basic. The macro.

The Design

The basic design of what I am calling a characteristic is a macro that is fundamentally four separate macros acting in unison. The first of which is the dispatch id. The primary function of the id is to have a long and unique name from which conflicts are unlikely or improbable. This is also the location at which you build the macros functionality. Let's look at an example of what a possible id might look like.

C++
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
#	if defined(__clang__)
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline \
        __attribute__((always_inline, flatten, hot)) inline 
#	elif defined(__GNUC__) || defined(__GNUG__)
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline \
       __attribute__((always_inline, flatten, optimize("-O3"))) inline 
#	elif (defined(_MSC_VER) && defined(_MSVC_LANG)) 
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline \
        __pragma(auto_inline(on))__pragma(inline_recursion(on)) __forceinline 
#	else
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline inline 
#	endif
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline

This macro defines a combination of attributes, pragmas, and keywords to comprise a forced inlining macro. This macro will target Msvc, Gcc, and Clang. It works stunningly on the given compilers, although it is the belief of this author that the keyword explicit should be used as a modifier for inline, amongst other things, to achieve this functionality in language proper. But I digress, all of the macro is upper case naming, with the exception of the trailing name. Take note of it as there will be more on this later.

The next macro is the forwarding macro. Its job is just what the name implies. It forwards the given token. In this macro, the given token will be concatenated with the prefix CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_ which will expand to our corresponding id.

C++
#	define CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__( __DISPATCH_ID__, ... ) \
CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_ ## \__DISPATCH_ID__ 
__VA_ARGS__ __DISPATCH_ARGS__EMPTY__

The next macro is the dispatch macro itself. Which calls the forwarding macro. This call through adds the second set of parentheses completing the call to the forwarder. This gives us the attribute style syntax while ensuring the desired expansion on a variety of compilers.

C++
#   define CHARACTERISTIC_DISPATCHER( ... ) \
CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__ __VA_ARGS__ __DISPATCH_ARGS__EMPTY__ 

The final macro is the access macro which exists for the sole purpose of undefining. As the id names are long, they are unlikely to ever cause naming conflicts. The access macro in contrast should be short and reflect the name of the project or library for which it is used. This access macro can be undefined and redefined on an as needed basis. Note that this is for demonstration purposes, as a more suitable, and shorter, library name should be used.

C++
#ifndef __characteristic__
// characteristic : dispatch macro - abstract pragmas, 
// declspecs, attributes, prerequisites, and feature checks.
// Uses names long enough to discourage name pollution while keeping the entries meaningful.
#	define __characteristic__( ... ) CHARACTERISTIC_DISPATCHER( __VA_ARGS__ )
#endif // __characteristic__

Now we have a macro that we can define and undefine easily in our code, while the longer names persist. For added measure, here is an id that checks for the availability of constexpr, and if it is found does a force inline constexpr, else falls back to the forced inlining scheme alone.

C++
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
#if defined(__cpp_constexpr) && __cpp_constexpr >= 201304     
#  define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr 
#         CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline constexpr
#else
#  define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr 
#         CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
#endif
#endif    // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr

The dispatch macros can be condensed down to one macro. The identifiers are the last name after the last underscore. These are to be lowercase, as macros used as identifiers will expand before the full dispatch id is concatenated. This may be the desired effect. But most often is not, so using lowercase id endings is recommended so that an unwanted macro doesn't expand in place of the characteristic identifier. For this reason, the attribute syntax was chosen.

Using the Code

Although this is a contrived example, that could probably be done much simpler, the core idea is that the characteristic expands to a longer more unique macro that won't pollute the code base. This helps to keep the code legible without fear the name will reoccur. While the shorter, more legible name can be undefined. A more suitable usage would be decorating the structures and functions of std::invoke for a zero overhead invoke. That just leaves the function call in its place. That being said, here is a simple factorial with our aggressive optimization.

C++
//__characteristic__((inline))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
#	if defined(__clang__)
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline 
#       __attribute__((always_inline, flatten, hot)) inline 
#	elif defined(__GNUC__) || defined(__GNUG__)
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline 
#              __attribute__((always_inline, flatten, optimize("-O3"))) inline 
#	elif (defined(_MSC_VER) && defined(_MSVC_LANG)) 
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline 
#              __pragma(auto_inline(on))__pragma(inline_recursion(on)) __forceinline 
#	else
#		define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline inline 
#	endif
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline 
 
//__characteristic__((constexpr))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
#if defined(__cpp_constexpr) && __cpp_constexpr >= 201304     
#  define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr 
#         CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline constexpr
#else
#  define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr 
#         CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_inline
#endif
#endif // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_constexpr
 
#ifndef __characteristic__
#	define CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__( __DISPATCH_ID__, ... ) 
#          CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_ ## 
#          __DISPATCH_ID__ __VA_ARGS__ __DISPATCH_ARGS__EMPTY__
#   define __DISPATCH_ARGS__EMPTY__
#	define CHARACTERISTIC_DISPATCHER_FUNCTION_MACRO( ... ) 
#          CHARACTERISTIC_FORWARD_TO_ATTRIBUTE_DISPATCHER__ 
#          __VA_ARGS__ __DISPATCH_ARGS__EMPTY__
/*
characteristic : dispatch macro - abstract pragmas, 
declspecs, attributes, prerequisites, and feature checks.
Uses names long enough to discourage name pollution while keeping the entries meaningful.
*/
#	define __characteristic__( ... ) CHARACTERISTIC_DISPATCHER_FUNCTION_MACRO( __VA_ARGS__ )
#endif // __characteristic__
 
#include <type_traits>
#include <iostream>
#include <stdexcept>
 
// force inline constexpr, or force inline
__characteristic__((constexpr)) // ids must be lower case to prevent unwanted expansions
int factorial(int n)
{
	return n <= 1 ? 1 : (n * factorial(n - 1));
}
 
// output function that requires a compile-time constant, for testing
template<int n>
struct ConstNOut
{
	__characteristic__((inline)) ConstNOut() { std::cout << n << '\n'; }
};
 
int main()
{
	std::cout << "4! = ";
	ConstNOut<factorial(4)> out1; // computed at compile time via constexpr
 
	volatile int k = 8; // falls back to force inline
	std::cout << k << "! = " << factorial(k) << '\n'; // inlined
}
 
#undef __characteristic__

Even though these are reserved keywords, they expand to our corresponding id. So they overwrite nothing while having meaningful names. Here, a wrapped inline means forced inline. A wrapped constexpr means force inline constexpr. Where either unwrapped would be without the forced inlining. Macros for noinline, nodiscard, and other common ids are trivial to build. As an added bonus, the keywords have additional syntax highlighting as do attribute specifiers that have corresponding type names. Again keywords, class, and variable names are preferred as they are unlikely to be affected by other macros. This is the reason lowercase lettering is used on the trailing section of the identifier, as lowercase is less likely to be another macro. But these don't have to be limited to single identifiers. They can support function style macros with parameters. Such as...

C++
//__characteristic__((error("output this error...")))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error
#   if defined(_MSC_VER) && !defined( __GNUC__ ) && 
#   !defined( __GNUG__ ) && !defined( __clang__ )
#       define     CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error( MESSAGE ) 
#                  __pragma(message(": error: "		\
MESSAGE))
#   else  // _MSC_VER
#       define     CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error( MESSAGE ) 
#                  _Pragma( CHARACTERISTIC_STRINGIZE
#                  ( GCC error(CHARACTERISTIC_EXPAND__(MESSAGE)) ) ) 
#   endif //_MSC_VER
#endif    // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_error
 
//__characteristic__((warning("output this warning...")))
#ifndef CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_warning
#   if defined(_MSC_VER) && !defined( __GNUC__ ) && 
#   !defined( __GNUG__ ) && !defined( __clang__ )
#       define     CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_warning
#                  ( MESSAGE ) __pragma(message(": warning: "		\
MESSAGE))
#   else  // _MSC_VER
#       define     CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_warning( MESSAGE ) 
#                  _Pragma( CHARACTERISTIC_STRINGIZE
#                  ( GCC warning(CHARACTERISTIC_EXPAND__(MESSAGE)) ) ) 
#   endif //_MSC_VER
#endif    // CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_message

And the stringize it depends on...

C++
#ifndef CHARACTERISTIC_STRINGIZE
/*	basic stringize macro	*/
#	define CHARACTERISTIC_EXPAND_UNUSED__(Token) Token  
#	define CHARACTERISTIC_EXPAND__(x)  CHARACTERISTIC_EXPAND_UNUSED__(x)  
#	define CHARACTERISTIC_STRINGIZE_UNUSED__(String) # String  
#	define CHARACTERISTIC_STRINGIZE(x) CHARACTERISTIC_STRINGIZE_UNUSED__(x)    
#	define CHARACTERISTIC_MACRO_ATTRIBUTE_DISPATCH_stringize
#                        ( RefString ) CHARACTERISTIC_STRINGIZE( RefString )   
#endif // CHARACTERISTIC_STRINGIZE

The usage...

C++
#if !defined(__cplusplus)
	__characteristic__((error("C++ compiler required.")))
#elif !defined(_MSC_VER) && !defined( __GNUC__ ) && 
#!defined( __GNUG__ ) && !defined( __clang__ )
	__characteristic__((warning("Unknown compiler details.")))
#endif

Points of Interest

    Feel free to try the sample on Compiler Explorer. If you do, the __pragma statements will have to be removed from the Msvc version of inline, as CE balks at them. The actual compiler does accept them and they are needed to unconditionally turn on inlining as the __forceinline keyword only works if the "only inline", or "any suitable", compiler switches are chosen. A direct copy and paste requires you to clean up the line endings that this site uses. Originally, this was designed to be used with just with attributes. But it came in handy so often, with other uses, that I thought to share. Hearing feedback on breaking the naming convention is one of the main goals of this exercise.

    For an actual use case see Wild West Coding: E.B.C.O. Compression. The characteristics header will be updated along with the articles they are associated with. The library uses the access point JOE, as opposed to __characteristic__, as it reflects the name of the library to witch it is contained.

History

  • 22nd August, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

RegularJoe5150
United States United States
No Biography provided

Comments and Discussions

 
BugArticle doesn't conform to standard naming rules Pin
Eric Kenslow23-Aug-21 14:14
MemberEric Kenslow23-Aug-21 14:14 
GeneralRe: Article doesn't conform to standard naming rules Pin
RegularJoe515023-Aug-21 16:32
MemberRegularJoe515023-Aug-21 16:32 
GeneralRe: Article doesn't conform to standard naming rules Pin
Eric Kenslow23-Aug-21 17:11
MemberEric Kenslow23-Aug-21 17:11 
GeneralRe: Article doesn't conform to standard naming rules Pin
RegularJoe515023-Aug-21 17:58
MemberRegularJoe515023-Aug-21 17:58 
GeneralRe: Article doesn't conform to standard naming rules Pin
RegularJoe515025-Aug-21 14:20
MemberRegularJoe515025-Aug-21 14:20 
Praiselooking forward to your next article! Pin
Southmountain22-Aug-21 11:55
MemberSouthmountain22-Aug-21 11:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.