TripShock
TripShock

Reputation: 4249

Avoiding ODR violations when using debug asserts

I have a header-only library that has some additional fail-fast runtime assertions enabled when compiled in debug mode. A simplified version of the header looks like this:

#include <exception>

#ifdef MYDEBUG
#   define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)
#else
#   define MYASSERT(condition)
#endif

template<typename T>
class Checker
{
public:

    T operator()(T value)
    {
        MYASSERT(value);
        return value;
    }
};

If one translation unit includes the header without defining MYDEBUG first, and another one includes it after defining MYDEBUG, and I link the resultant object files together, would that constitute an ODR violation?

How can I avoid this but still allow each TU to independently specify its desired assertion settings when including the header?

Upvotes: 3

Views: 517

Answers (4)

user877329
user877329

Reputation: 6200

Well, you sort of can, by making sure the function is not ODR-used in the first place. This means that you must make sure the function is always inlined.

  1. A precondition for this is that it is an inline function from the standard perspective. Your function already satisfies this.

  2. Depending on the compiler and compiler flags, you may also need a special attribute on the function in order to force it to be inline. For gcc, you can use [[gnu::always_inline]].

  3. You must never store the address of the function

If we ignore the fact that it is possible to take the address of anything, it is a quite useful technique to ensure that the caller behaves as it should.

#include <cassert>

void do_work(int);

[[gnu::always_inline]] inline void do_work_checked(int value)
{
    assert(value);
    do_work(value);
}

void foo(int val)
{
    do_work_checked(val);
}

Even without optimization, do_work_checked does not appear in the assembly.

In order to prevent (3), one way is to instead use a macro

#include <cassert>

void do_work(int);

#define do_work_checked(value) \
{ \
   assert(value); \
   do_work(value); \
}

void foo(int val)
{
    do_work_checked(val);
}

Since there is no function anymore, there cannot be any ODR issues.

Upvotes: 0

rustyx
rustyx

Reputation: 85382

Solution 1: use scoping:

#ifdef MYDEBUG
#   define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)
#else
#   define MYASSERT(condition)
#endif

namespace {
  template<typename T>
  class Checker
  {
  public:
      T operator()(T value)
      {
          MYASSERT(value);
          return value;
      }
  };
}

This essentially changes Checker to internal linkage, and potentially comes with extra cost, that is it can end up in the final executable multiple times. However, in this particular case there's no extra cost as it will probably be inlined anyway.

Solution 2: parameterize the template on the debug mode:

(Update 3: using template specialization thanks to @Jarod42's suggestion)

#ifdef MYDEBUG
#   define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)
#   define MYDEBUG_FLAG true
#else
#   define MYASSERT(condition)
#   define MYDEBUG_FLAG false
#endif

template<typename T, bool = MYDEBUG_FLAG> class Checker;

template<typename T>
class Checker<T, MYDEBUG_FLAG>
{
public:
    T operator()(T value)
    {
        MYASSERT(value);
        return value;
    }
};

Then the debug- and non-debug instantiations will be independent of each other.

The nice thing about this is even if one accidentally instantiates Checker<T, !MYDEBUG_FLAG>, it won't compile and hence won't violate ODR (provided only one version, either debug- or non-debug, is ever defined in each TU).

Upvotes: 2

Jarod42
Jarod42

Reputation: 217478

Variant of first RustyX's answer, but fixed I think:

#ifdef MYDEBUG
#   define MYDEBUG_FLAG true
#else
#   define MYDEBUG_FLAG false
#endif

#define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)

// Following declaration differs, but doesn't break ODR.
template<typename T, bool = MYDEBUG_FLAG> class Checker;

// And both definitions of specialization.
template <typename T>
class Checker<T, true>
{
public:

    T operator()(T value)
    {
        MYASSERT(value);
        return value;
    }
};

template <typename T>
class Checker<T, false>
{
public:

    T operator()(T value)
    {
        return value;
    }
};

Upvotes: 0

R Sahu
R Sahu

Reputation: 206627

If one translation unit includes the header without defining MYDEBUG first, and another one includes it after defining MYDEBUG, and I link the resultant object files together, would that constitute an ODR violation?

Yes, it is a violations of the one-definition-rule. It's a violation of the rule for inline functions that says the inline function definitions must have the exact tokens in all the translation units.

How can I avoid this but still allow each TU to independently specify its desired assertion settings when including the header?

One way to deal with is to define MYASSERT to be file scoped static functions.

#ifdef MYDEBUG
static void MYASSERT(bool condition)
{
   if (!(condition))
   {
      std::terminate();
   }
}
#else
static void MYASSERT(bool condition)
{
   // Noop
}
#endif

It appears that you cannot. Thanks, @RustyX.

Upvotes: 3

Related Questions