Search code examples
c++language-lawyerundefined-behaviorunnamed-namespace

Is declaring a friend which is forward-declared in an unnamed namespace an ODR-violation?


I've been using a dirty trick where I use unnamed namespaces to specify different behavior for each file (it is for unit-testing). It feels like it shouldn't be well-defined, but it works on every major compiler released in the past six years.

I first forward-declare a bunch of classes in an anonymouse namespace:

namespace {
class context_check;
class context_block;
class register_test;
} // namespace

Then I declare a class and make these classes friends:

struct test_case
{
    // ...
};

class context {
// ...
private:
    // TODO: this feels like ODR-violation...
    friend class context_check;
    friend class context_block;
    friend class register_test;

private:
    void add_test(test_case&& test)
    {
        // ...
    }

    // ...
};

And in each cpp file I have following declarations (for tha sake of simplicity I copy and pasted them here, but in reality I declare the content of unnamed namespace in a header):

// A.cpp

namespace {
extern context file_context;

class register_test {
public:
    register_test(const char* test_name, test_case&& test)
    {
        file_context.add_test(std::move(test));
    }
};

} // namespace

register_test register_test_1( test_case{"test1"} );
// B.cpp

namespace {
extern context file_context;

class register_test {
public:
    register_test(const char* test_name, test_case&& test)
    {
        file_context.add_test(std::move(test));
    }
};

} // namespace

register_test register_test_2( test_case{"test2"} );

So each TU has its own definitions of register_test, context_check etc. Is this well-defined? Feels like it should be an UB...


Solution

  • Seems to violate the following condition required by ODR, as stated on https://en.cppreference.com/w/cpp/language/definition:

    There can be more than one definition in a program of each of the following: class type, [...], as long as all of the following is true:

    • [...]
    • name lookup from within each definition finds the same entities (after overload-resolution), except that [...]

    None of the exceptions are relevant and lookup for e.g. context_check in friend class context_check;, which is part of the definition of context, finds an internal linkage entity, which cannot be the same if the definition is included in multiple translation units.

    The actual corresponding rule of the standard can be found e.g. in [basic.def.odr]/13.9 of the post-C++20 draft.