3

In C++ one sometimes has annoying bugs where someone forgot to close out a namespace which was opened in a header file, and it can sometimes be difficult to track down exactly which one.

Somewhat less banally, sometimes there are macros which for technical reasons cannot appear enclosed within any namespace, or cryptic error messages may occur. In the boost fusion library, BOOST_FUSION_DEFINE_STRUCT requires this, for example. Instead you are supposed to use the macro at filescope, and pass it the namespace that you want the declaration to be within.

For that matter, you get undefined behavior if you include any standard library header within a namespace.

Is it possible to do something like, write a macro e.g. ASSERT_FILESCOPE so that I can put a line ASSERT_FILESCOPE; at the end of a C++ header file, or at the start of a macro like BOOST_FUSION_DEFINE_STRUCT, which will cause a static_assert failure with a nice error message if that expression is not at filescope?

Chris Beck
  • 15,614
  • 4
  • 51
  • 87
  • Since your goal is an error either way, does this count as a decent error? *error: '::assert_filescope_function' has not been declared -- static_assert(stdx::is_function_v, "Must be file scope")* You could name the function something more explicit, too. – chris Feb 21 '16 at 02:54
  • @chris: Yeah so I guess making a test declaration and checking its visibility is the natural way to go, but the parts I wasn't sure about were, can you do it without colliding with other instances of this macro within the same compilation unit, can you do it without causing ODR problems... I mean I guess that you can use some SFINAE trick to convert visibliity of the symbol into a boolean value for the static assert. Maybe this is easy but it seems like there might be some subtleties, and I was surprised this isn't a dupe (so far as I know) – Chris Beck Feb 21 '16 at 02:59
  • I can't think of any way to include SFINAE with the qualified name in there. A declaration in the macro should be fine if the name doesn't collide. It's never defined and never odr-used, so ODR should be fine. Oh, but if this appears in a namespace after the macro has already been used at global scope, it would find that one. – chris Feb 21 '16 at 03:13

2 Answers2

4

One possibility is to declare a name and then test for ::name. To avoid the same name being declared in the global scope before a later use of the macro in the same translation unit, you can use __COUNTER__, or __LINE__ if that works better for you. It's not perfect by any means, but at least the first line of the error contains the message. live example

#include <type_traits>

#define CONCAT(x, y) CONCAT_I(x, y)
#define CONCAT_I(x, y) x##y

#define ASSERT_FILESCOPE \
    ASSERT_FILESCOPE_I(__COUNTER__)

#define ASSERT_FILESCOPE_I(counter) \
    static constexpr bool CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)() { return true; } \
    static_assert(::CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)(), "Detected a location other than file scope.")

I get this error from Clang:

main.cpp:17:5: error: no member named 'ASSERTION_FAILED_NOT_AT_FILE_SCOPE0' in the global namespace; did you mean simply 'ASSERTION_FAILED_NOT_AT_FILE_SCOPE0'?
    ASSERT_FILESCOPE;
    ^~~~~~~~~~~~~~~~
main.cpp:7:5: note: expanded from macro 'ASSERT_FILESCOPE'
    ASSERT_FILESCOPE_I(__COUNTER__)
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:11:19: note: expanded from macro 'ASSERT_FILESCOPE_I'
    static_assert(::CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)(), "Detected a location other than file scope.")
                  ^~
main.cpp:17:5: note: 'ASSERTION_FAILED_NOT_AT_FILE_SCOPE0' declared here
main.cpp:7:5: note: expanded from macro 'ASSERT_FILESCOPE'
    ASSERT_FILESCOPE_I(__COUNTER__)
    ^
main.cpp:10:20: note: expanded from macro 'ASSERT_FILESCOPE_I'
    static constexpr bool CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)() { return true; } \
                   ^
main.cpp:3:22: note: expanded from macro 'CONCAT'
#define CONCAT(x, y) CONCAT_I(x, y)
                     ^
main.cpp:4:24: note: expanded from macro 'CONCAT_I'
#define CONCAT_I(x, y) x##y
                       ^
<scratch space>:101:1: note: expanded from here
ASSERTION_FAILED_NOT_AT_FILE_SCOPE0
^

and this one from GCC:

main.cpp:11:19: error: '::ASSERTION_FAILED_NOT_AT_FILE_SCOPE0' has not been declared
     static_assert(::CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)(), "Detected a location other than file scope.")
                   ^
main.cpp:7:5: note: in expansion of macro 'ASSERT_FILESCOPE_I'
     ASSERT_FILESCOPE_I(__COUNTER__)
     ^
main.cpp:17:5: note: in expansion of macro 'ASSERT_FILESCOPE'
     ASSERT_FILESCOPE;
     ^
main.cpp:11:19: note: suggested alternative:
     static_assert(::CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)(), "Detected a location other than file scope.")
                   ^
main.cpp:7:5: note: in expansion of macro 'ASSERT_FILESCOPE_I'
     ASSERT_FILESCOPE_I(__COUNTER__)
     ^
main.cpp:17:5: note: in expansion of macro 'ASSERT_FILESCOPE'
     ASSERT_FILESCOPE;
     ^
main.cpp:10:34: note:   'foo::ASSERTION_FAILED_NOT_AT_FILE_SCOPE0'
     static constexpr bool CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)() { return true; } \
                                  ^
main.cpp:4:24: note: in definition of macro 'CONCAT_I'
 #define CONCAT_I(x, y) x##y
                        ^
main.cpp:10:27: note: in expansion of macro 'CONCAT'
     static constexpr bool CONCAT(ASSERTION_FAILED_NOT_AT_FILE_SCOPE, counter)() { return true; } \
                           ^
main.cpp:7:5: note: in expansion of macro 'ASSERT_FILESCOPE_I'
     ASSERT_FILESCOPE_I(__COUNTER__)
     ^
main.cpp:17:5: note: in expansion of macro 'ASSERT_FILESCOPE'
     ASSERT_FILESCOPE;
     ^
chris
  • 60,560
  • 13
  • 143
  • 205
1

Here's a version which pools part of my initial idea with @chris ' idea. It's not clear if it is actually any better, other than in actually triggering a static assertion when it fails, and at the cost of being slightly more complicated. YMMV:

// Fall through to this value if we can't see the test symbol
template <int N>
struct assert_filescope_check_ {
  static constexpr bool value = false;
};

#define ASSERT_FILESCOPE_CONCAT(x, y) ASSERT_FILESCOPE_CONCAT_I(x, y)
#define ASSERT_FILESCOPE_CONCAT_I(x, y) x##y

#define ASSERT_FILESCOPE \
    ASSERT_FILESCOPE_I(__COUNTER__)

#define  ASSERT_FILESCOPE_I(counter)                                  \
    template <int> struct assert_filescope_check_;                    \
                                                                      \
    template <>                                                       \
    struct assert_filescope_check_<counter> {                         \
      static constexpr bool value = true;                             \
    };                                                                \
                                                                      \
    static_assert(::assert_filescope_check_<counter>::value,          \
                  "Assert filescope failed!")

The main idea is that, there is a template defined in the header at filescope, which we try to partially specialize using the value of counter. If we then can see the partial specialization, then we pass. Before we attempt to partially specialize, we must redeclare the primary template (but not define it) so that in the case that we are actually inside a namespace, we end up just defining a new symbol.

When I first tried, I wanted to avoid using __COUNTER__ and __LINE__ and use template trickery for that instead, but that made it too complicated. And I guess anyways it is well-understood how to do that, just a bit tedious. 1 2.

If you need to avoid __COUNTER__ and have problems with __LINE__ then I guess you can just drop in one of those solutions into either @chris' or my version, in place of the __COUNTER__ instance. So, nothing lost. :)

Community
  • 1
  • 1
Chris Beck
  • 15,614
  • 4
  • 51
  • 87