2
\$\begingroup\$

I designed a class for debug builds which does not incur any overhead in release mode. A usecase is for example: I have a function and I want to count how often it is called. For this I could write the following

function void f() {
    #ifndef NDEBUG
        static int i = 0;
        ++i;
        std::cout << "Counter: " << i << "\n";
    #endif
    // do something
}

But that clobbers up my source code with preprocessor directives. Thus I had the idea for a class such that the example above becomes:

function void f() {
    static Nop_t<int> i = 0;
    std::cout << "Counter: "_nop << i << "\n"_nop;
    ++i;
    // do something
}

By default, in Debug Mode Nop_t< T > is the same as T, otherwise it is a type which does nothing and accepts all possible code constructs.

Now the real implementation and a test file (I wrote the tests in Google Test and adapted them for posting here).

  • The class uses some utilities (mostly taken from Stackoverflow)
///
/// Utilities
///

 /* T_NDEBUG
 * Macro which is always defined.
 * It has the value true if NDEBUG is defined, and false otherwise 
 */
 #ifndef NDEBUG
     #define T_NDEBUG true
 #else
     #define T_NDEBUG false
 #endif

/** Standard preprocessor string concatenation macro which expands also macro arguments
 * Example: T_JOIN( abc123, __LINE__ )
 * Yields: abc123116  // or something else if the line of this macro changes
 */
#define T_JOIN( x, y ) T_JOIN_AGAIN( x, y )
#define T_JOIN_AGAIN( x, y ) x ## y

/**
 * Stuff for macro with variable number of arguments
 * @Link stackoverflow.com/questions/11761703
 *
 * @Example: We want to have macros SUM(a), SUM(a,b), SUM(a,b,c).
 *
 *        ```
 *        #define SUM( ... )       OVERLOADED_MACRO( SUM, __VA_ARGS__ )
 *
 *        #define SUM1( a )        (a)
 *        #define SUM2( a, b )     (a)+(b)
 *        #define SUM3( a, b, c )  (a)+(b)+(c)
 *        ```
 * Note: Macros without arguments are not possible in a portable way
 */

#define OVERLOADED_MACRO( M, ... ) _OVR( M, _COUNT_ARGS(__VA_ARGS__) ) ( __VA_ARGS__ )
#define _OVR( macroName, number_of_args )   _OVR_EXPAND(macroName, number_of_args)
#define _OVR_EXPAND( macroName, number_of_args )    macroName##number_of_args

#define _COUNT_ARGS( ... )  _ARG_PATTERN_MATCH( __VA_ARGS__, 9,8,7,6,5,4,3,2,1 )
#define _ARG_PATTERN_MATCH( _1,_2,_3,_4,_5,_6,_7,_8,_9, N, ... )   N

/** Macro removing "unused" warning in Debug Mode
 * Macro does not work reliable
 *
 * @example
 * function f( int a ) {
 *     MAYBE_UNUSED( a );
 * }
 */
#ifndef MAYBE_UNUSED //macro for disabling "unused function warning"
    #define MAYBE_UNUSED(expr) do{ (void)(expr); } while (0)
#endif
  • Nop class
///
/// Nop class
///

#include <iosfwd>
#include <utility>

/**
 * # Nop.hpp
 *
 * ## General description
 * This is a class which is either
 * - a typedef for a type, or
 * - a type which takes everything and does nothing (i.e. every use of a variable of this type is optimized away by the compiler)
 * ```
 * Nop_t< T, true >  // is a typedef for T
 * Nop_t< T, false >  // is the do-nothing type
 * Nop_t< T >  // is a typedef in Debug Mode, and the do-nothing type in Release mode
 * ```
 * 
 * ## Usage example
 * static Nop_t< std::atomic<int> > counter;
 * void func() {
 *     counter++;  // count how often this function is called, but only in Debug mode
 *     std::cout << (Nop_t<std::string>)"counter: " << counter;  // text is only printed in Debug Mode
 *     // do something
 * }
 * 
 * ## User defined literals
 * By using the macro NOP_LITERAL a user defined literal can be generated.
 *     NOP_LITERAL( name, condition )
 *     NOP_LITERAL( name )  // name defaults to "nop"
 * If condition is true, then the literal will become the usual literal, otherwise it will become a Nop_t
 * Example:    
 *     NOP_LITERAL( nop )
 *     void f() {
 *         Nop_t< int > value;
 *         std::cout << "Value: "_nop << value;  // Does nothing in Release mode
 *     }
 * Notes:
 *     This macro shall only be used in cpp files, otherwise the global namespace may get polluted.* 
 * 
 * ## Adding supported functions
 *  If a function / member function / typedef is missing, there are several ways to add it to the class
 *  1) Add it inside the class using the macros NOP_MEMBER_FUNCTION, NOP_FRIEND_FUNCTION, NOP_UNARY, NOP_BINARY_OPERATOR, NOP_TYPEDEF
 *  2) Add it inside the class handwritten. Such functions should be made as generic as possible
 *  3) Add it outside of the class using the macros NOP_FUNCTION, NOP_LAMBDA
 */

namespace detail {

    template< typename T >
    struct Nop_impl {
        // char make_struct_to_size_zero[0];  // Adding this member would make this struct have size 0, but this is not Standards compliant
        
        // This function shall accept anything and do nothing.
        // Type checking is not necessary, since it is done when "this class is disabled" and the underlying type is used (i.e. in Debug mode).
        template< typename ... Types > constexpr Nop_impl( Types && ... ) noexcept {}
        
        // This macro adds a typedef
        #define NOP_TYPEDEF( name, type ) using name = type;
        NOP_TYPEDEF( value_type, T )
        #undef NOP_TYPEDEF
        
        // This macro defines a member function, taking any arguments
        #define NOP_MEMBER_FUNCTION( func ) template< typename ... Types > constexpr Nop_impl func( Types && ... ) const noexcept { return Nop_impl{}; }
        NOP_MEMBER_FUNCTION( operator() )
        NOP_MEMBER_FUNCTION( empty )
        NOP_MEMBER_FUNCTION( size )
        #undef NOP_MEMBER_FUNCTION
    
        // Defining a function here takes any arguments, one of which a Nop_t,
        // defining a function outside the class using the NOP_FUNCTION macro, generates only a function taking one argument, a Nop_t.
        #define NOP_FRIEND_FUNCTION( func ) template< typename ... Types > friend constexpr Nop_impl func( Types && ... ) noexcept { return Nop_impl{}; }
        NOP_FRIEND_FUNCTION( sin )
        NOP_FRIEND_FUNCTION( swap )
        #undef NOP_FRIEND_FUNCTION
        
        // This macro defines a unary operator
        #define NOP_UNARY( op ) constexpr Nop_impl op noexcept { return Nop_impl{}; }
        NOP_UNARY( operator+() )
        NOP_UNARY( operator-() )
        NOP_UNARY( operator++() )
        NOP_UNARY( operator++(int) )  // NOLINT(cert-dcl21-cpp)
        NOP_UNARY( operator--() )
        NOP_UNARY( operator--(int) )  // NOLINT(cert-dcl21-cpp)
        NOP_UNARY( operator!() )
        NOP_UNARY( operator~() )
        #undef NOP_UNARY
    
        // This macro defines a binary operator
        #define NOP_BINARY_OPERATOR( op ) template< typename L, typename R >  friend constexpr Nop_impl operator op ( L &&, R && ) noexcept { return Nop_impl{}; }
        NOP_BINARY_OPERATOR( + )
        NOP_BINARY_OPERATOR( - )
        NOP_BINARY_OPERATOR( * )
        NOP_BINARY_OPERATOR( / )
        NOP_BINARY_OPERATOR( % )
        NOP_BINARY_OPERATOR( += )
        NOP_BINARY_OPERATOR( -= )
        NOP_BINARY_OPERATOR( *= )
        NOP_BINARY_OPERATOR( /= )
        NOP_BINARY_OPERATOR( %= )
        
        NOP_BINARY_OPERATOR( < )
        NOP_BINARY_OPERATOR( <= )
        NOP_BINARY_OPERATOR( >= )
        NOP_BINARY_OPERATOR( > )
        NOP_BINARY_OPERATOR( != )
        NOP_BINARY_OPERATOR( == )
        
        NOP_BINARY_OPERATOR( && )
        NOP_BINARY_OPERATOR( || )
        NOP_BINARY_OPERATOR( & )
        NOP_BINARY_OPERATOR( | )
        NOP_BINARY_OPERATOR( ^ )

        NOP_BINARY_OPERATOR( &= )
        NOP_BINARY_OPERATOR( |= )
        NOP_BINARY_OPERATOR( ^= )
        #undef NOP_BINARY_OPERATOR          
    
        // Section for special functions
        template< typename U > constexpr Nop_impl operator[]( const U & ) noexcept { return Nop_impl{}; }
        friend constexpr std::ostream& operator<<( std::ostream & os, Nop_impl ) noexcept { return os; };
    
    };
    
    // Since partial specialization of typedefs/using-directions is not allowed,
    // we use a helper struct with a function returning the wanted type.
    // This function can be used in a using directive then.
    template< typename T, bool E = !T_NDEBUG > struct make_nop;

    template< typename T >
    struct make_nop< T, true > {
        template< typename ... Args > constexpr static T make( Args && ... args ) noexcept( noexcept(T( std::forward< Args >( args ) ... )) ) { 
            return T( std::forward< Args >( args ) ... ); 
        };
    };
    
    template< typename T >
    struct make_nop< T, false > {
        template< typename ... Args > constexpr static Nop_impl< T > make( Args && ... args ) noexcept { 
            return Nop_impl< T >( std::forward< Args >( args ) ... ); 
        };
    };
}

template< typename T, bool E = !T_NDEBUG > using Nop_t = decltype( detail::make_nop<T,E>::make() );


// This macro defines a function taking exactly one argument of type Nop_t< T, false >, where T is any type
#define NOP_FUNCTION( func ) template< typename T > static inline constexpr \
    ::detail::Nop_impl< T > func( const ::detail::Nop_impl<T> & ) noexcept { return ::detail::Nop_impl< T >{}; }

// This macro defines user defined literals
// Example:
#define NOP_LITERAL( ... )       OVERLOADED_MACRO( NOP_LITERAL, __VA_ARGS__ )
#define NOP_LITERAL1( name ) \
    NOP_LITERAL2( name, !T_NDEBUG )
#define NOP_LITERAL2( name, condition ) \
    namespace nop { namespace T_JOIN( line, __LINE__ ) { \
        inline constexpr Nop_t< unsigned long long int, condition > operator""_ ## name ( unsigned long long int lit ) { return Nop_t< unsigned long long int, condition >{ lit }; } \
        inline constexpr Nop_t< long double, condition > operator ""_ ## name( long double lit ) { return Nop_t< long double, condition >{ lit }; } \
        inline constexpr Nop_t< char, condition > operator ""_ ## name( char lit ) { return Nop_t< char, condition >{ lit }; } \
        inline constexpr Nop_t< wchar_t, condition > operator ""_ ## name( wchar_t lit ) { return Nop_t< wchar_t, condition >{ lit }; } \
        inline constexpr Nop_t< char16_t, condition > operator ""_ ## name( char16_t lit ) { return Nop_t< char16_t, condition >{ lit }; } \
        inline constexpr Nop_t< char32_t, condition > operator ""_ ## name( char32_t lit ) { return Nop_t< char32_t, condition >{ lit }; } \
        inline constexpr Nop_t< const char *, condition > operator ""_ ## name( const char * lit, size_t ) { return Nop_t< const char *, condition >{ lit }; } \
        inline constexpr Nop_t< const wchar_t *, condition > operator ""_ ## name( const wchar_t * lit, size_t ) { return Nop_t< const wchar_t *, condition >{ lit }; } \
        inline constexpr Nop_t< const char16_t *, condition > operator ""_ ## name( const char16_t * lit, size_t ) { return Nop_t< const char16_t *, condition >{ lit }; } \
        inline constexpr Nop_t< const char32_t *, condition > operator ""_ ## name( const char32_t * lit, size_t ) { return Nop_t< const char32_t *, condition >{ lit }; } \
    } } \
    using nop:: T_JOIN( line, __LINE__ )::operator""_ ## name;
  • main/tests:
///
/// main / tests
///

#include <cassert>
#include <iostream>
#include <cmath>
#include <type_traits>
#include <vector>

NOP_LITERAL( nop )  // must compile
NOP_FUNCTION( atan )
NOP_LITERAL( nop1, true )
NOP_LITERAL( nop2, false )

int main() {

    static_assert( std::is_standard_layout<Nop_t<int,true>>::value, "Is standard layout" );
    static_assert( std::is_pod<Nop_t<int,true>>::value, "Is standard layout" );

    
    {
        struct S {};
        volatile Nop_t< S, false > n1( S{} );  // volatile to supress warning
        volatile Nop_t< S, false > n2;
        volatile Nop_t< int, false > n3( S{} );
        volatile Nop_t< int, false > n4( "asd" );
        MAYBE_UNUSED( n1 );
        MAYBE_UNUSED( n2 );
        MAYBE_UNUSED( n3 );
        MAYBE_UNUSED( n4 );
    }
    {
        struct S {};
        volatile Nop_t< S, true > n1( S{} );
        volatile Nop_t< S, true > n2;
        Nop_t< int, true > n3a( 1 );
        // Nop_t<int,true> n3b( S{} );
        // Nop_t<int,true> n3c( "asd" );
        Nop_t< std::string, true > n4;
        assert( n3a == 1 );
        assert( n4.empty() );
        MAYBE_UNUSED( n1 );
        MAYBE_UNUSED( n2 );
    }
    {
        Nop_t< float, true > t = 10;
        Nop_t< float, false > f = 10;
        (void) atan( t );
        (void) atan( f );
    }
    {
        Nop_t< std::vector<int>, true > vec1{1,2,3};
        Nop_t< std::vector<int>, false > vec2{1,2,3};
        assert( vec1[2] == 3 );
        (void) vec2[2];  // must compile
    }
    {
        Nop_t< std::vector<int>, true > vec1(3,1);
        Nop_t< std::vector<int>, false > vec2(3,1);
        assert( vec1[2] == 1 );
        MAYBE_UNUSED( vec2 );
    }
    {
        Nop_t< int, true > n = 2;
        int m = n;
        assert( n == 2);
        assert( m == 2);
    }
    {
        struct S {};
        Nop_t< const volatile S, false > s;
        sin( s );
        s.size();
    }
    {
        Nop_t< std::string, true > n1 = "ABC";
        Nop_t< std::string, false > n2 = "ERROR";
        
        std::cout << Nop_t< std::string, true >{"123"} << n1;
        std::cout << Nop_t< std::string, false >{"ERROR"} << n2 << std::endl;
        // Output must be "123ABC"  
    }
    {
        
        std::cout << (Nop_t< std::string, true >)"123";
        std::cout << (Nop_t< std::string, false >)"ERROR" << std::endl;
        // Output must be "123"     
    }
    {
        auto i1 = 123_nop1;
        assert( i1 == 123 );
        
        auto i2 = "ERROR"_nop2;
        std::cout << i2 << std::endl;
        // No output must be produced 
    }
    {
        // must compile
        struct S {};
        Nop_t< int, false > n;
        S s;
        n * 1 * n * n;
        1 * n * 1;
        n % 1 % n % n;
        s % n % s % s ;
        
        n += s += n;
        n -= s -= n -= n;
        n *= n;
        n /= n;
        
        sin( n );
        
        n( 1, 2 );
        n( s );
        
        !n;
        
        (void) (n==s<=n<s!=n>=n>s);
        
        n && n && s || n &= 2;
        
        n = 5;
    }
    {
        Nop_t< double, true > nf(0);
        Nop_t< double, true > nd(0);
        (void) sin( nf );
        (void) sin( nd );
    }
}

My main questions are:

  • Does the use of this class really occurs no overhead? Did I oversee something?
  • Is the interface sensible?
  • Is the documentation ok? Can the use of the class be understood from the documentation?
  • Are the user defined literals correctly implemented? Do they also no incur any overhead?
  • I found nothing like this class in the internet, but the idea is not very exotic. So I wonder: Is this a bad approach?
  • Is there a more easy approach for the definition of Nop_t which becomes a typedef for a type or a class than using the helper function make_nop
\$\endgroup\$
3
  • \$\begingroup\$ MACRO MAYBE_UNUSED is unneeded with C++17 attribute [[maybe_unused]] (You can even apply it to your class). \$\endgroup\$
    – Jarod42
    Jan 18 at 10:49
  • 1
    \$\begingroup\$ "otherwise it is a type which does nothing and accepts all possible code constructs." Doesn't accept member access (And I doubt you can anyway, or use operator -> (but need wrapper for enabled case too)). \$\endgroup\$
    – Jarod42
    Jan 18 at 10:55
  • \$\begingroup\$ Upvoted, not because I like the idea, but because you thought to have it reviewed. \$\endgroup\$
    – JDługosz
    Jan 18 at 15:10

2 Answers 2

4
\$\begingroup\$

I would like to get the elephant out of the room first by saying that the code doesn't really solve the problems it wants to solve. My post will compare the code in the question with the following code:

#ifdef NDEBUG
#define DEBUG_ONLY_STATEMENTS(statements)
#else
#define DEBUG_ONLY_STATEMENTS(statements) (statements)
#endif

Clarity of intent

I am not well versed in preprocessor metaprogramming so I will spend quite a bit of time understanding what the code in question does. If I am searching for a bug, it might lead me to assume that the bug is somewhere in the preprocessor generated code. My IDE shows a replacement, fortunately, but I believe it is still far from just either including the statements or just removing them completely.

Integration with tools

The piece of usage code shown in question will not be greyed out in release mode, hence it will lead to assumption that it is somehow important either way. Combined with previous point, this leads to a serious problem. With the NDEBUG version, the code will be greyed out and combined with its simplicity it will be clear that the code is really debug only.

Not so lightweight

May be it will not have any significant events at runtime, it might cause generation of extra symbols in the resulting executable. I am not familiar with ABI, but there might some other very subtle effects.

Unclear purpose

To debug a program I usually pull out gdb and try to come up with some fancy python to solve my problem. In the worst case I usually branch out and cherry pick last commits onto my branch. I rarely leave the debug code there (when left out it is usually commented out).

Instrumentation on the other hand is usually present regardless of compilation mode. It usually contains performance metrics along with some logging.


I'm not saying that macros are evil in all of the cases and I believe knowing how to use them is important, but this particular usage is not something I would put in my codebase.

Besides, the goal was to solve this problem: But that clobbers up my source code with preprocessor directives.. My impression was that the code in question made it worse, in a sense that there was more preprocessor involved.

\$\endgroup\$
5
  • \$\begingroup\$ 1) I share the concerns about the extra symbols, but I do not yet know how to adress them -- both in code, as well as in a question (for StackOverflow e.g.). 2) I do not understand the concerns about the preprocessor stuff. Most of the macros are just inside the class. If you mean the macros still used in the main function, those are mostly because of they are tests and to silence warnings. In preoduction code, one barely would need to use extra macros. \$\endgroup\$
    – tommsch
    Jan 18 at 13:43
  • \$\begingroup\$ @tommsch, being hidden in the class is the problem in this case. I believe the main intention was to make it simpler and less suprising, but looking at all of those preprocessor directives in the nop class would actually make me think about how it works and how it interacts with other stuff. I'd worried that I stepped on one of those C++ pitfalls that I still do not know even after using the language for about 5 years. The NDEBUG is very simple, which is the advantage here. I hope this makes it more clear. \$\endgroup\$ Jan 18 at 18:56
  • \$\begingroup\$ 2) That makes it clear. But I dont think thats a problem. These macros I use quite often and I dont think they do surprising stuff. I just added them to avoid code duplication. 3) One more note about instrumentation: That was actually the original intent why I invented this class. I want to count how often an inner loop is executed so I can compare different versions of my algorithms. I clearly do not want that to keep this in production code, but I also do not want to remove the code after my measurement, because I think I will need it again. \$\endgroup\$
    – tommsch
    Jan 18 at 19:06
  • \$\begingroup\$ @tommsch, I believe the lower level profiling could be done using existing tools like perf. Brandon Gregg has a lot of resources on that. Perf can be launched as a separate process and be attached to the app. It will shift the stress towards devops, but I believe it is worth it. If it is a performance metric, I would only think about it if it does either constant time operations or does something like context switch which is costly. YMMV, this is just my experience (about 1.5 years in deep learning deployment field). \$\endgroup\$ Jan 18 at 19:24
  • \$\begingroup\$ @tommsch it might not do anything surprising, but it will not convince people reading the code that it will not do anything surprising. \$\endgroup\$ Jan 18 at 19:30
2
\$\begingroup\$

One obvious problem lies here:

#define _OVR(

Names beginning with underscore followed by a capital letter are reserved for any purpose for the implementation. So this name is not available in a user header.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.