Showing posts with label Preprocessor. Show all posts
Showing posts with label Preprocessor. Show all posts

2025-02-28

Header-implementation duality?

While, in principle,1 C++20 marks the beginning of the end of our long, collective, inclusion nightmare the painful truth is that many C++ developers are going to be dealing with header files for years to come. And I've heard of no plans to bring a modules system to plain C at all.

But at least we have decades of experience from which to derive best practices and better tools to automate or enforce those practices. Right?

Some of the best practices:

  1. Every header should have include guards.2
  2. Don't #include files unless you need declarations or definitions found therein. Where possible prefer forward declarations or forward declaring headers (like iosfwd).
  3. Explicitly include headers to cover everything you need. Do not rely on transitive includes.

None of this is particularly controversial (leaving out the holy war over how to protect files from multiple inclusion), but I find that I have a minor interpretational difference with include-what-you-use (AKA IWYU: one of the leading tools for automating these rules). You see IWYU asks me to include in my implementation file things I already included in my the associated header file. Now, that is a strict interpretation of rule (3), so why does it bother me? Apparently I think of the header and implementation as two parts of a single logical unit rather than two independent things. I mean, those things should be maintained together so the danger of relying on the transitive inclusion is mitigated.

Of course, if I was implementing the tool I'd at the very least start with the current behavior. Simply because it's going to be very hard for the tool to know that this header really and truly does go with that implementation. I mean usual if they have the same stem that's a good clue, and in project where headers and implementations can sit in the same directories that is also helpful. But some layouts separate the interface files, so then what?


1 By now a non-trivial fraction of C++ coders are able to use the 2020 standard, but that doesn't mean that they are able to use modules. Support has been pretty slow even in compilers and build tools maintainers are stuggling with some real problems inherent in the pure flexibility of the module standard in C++.

2 Yes, I know, in the Windows world many people strongly advocate for #pragma once instead, and avoiding the possibility of name collisions is a big advantage in my book. But it remains non-standard which is why some major style guides and the core guideline still insist on classic approach.

2022-07-11

Little surprises #8: using the preprocessor to create latent bugs

Aside: As opposed to using the build system or the template processor which are totaly different except in the ways they are completely the same.


Languages with C-derived syntax and exceptions generally have a try-catch construct and the ones I'm familiar with have a little trap for the inattentive programmer: the catch block is a different scope from the try blocks. Which means when you merrily write try { ErrorInfo e; someRiskyOperation(e) } catch (...) { print("Risky operation failed with error %s\n",e.explanation()); } because the argument to someRiskyOperation is an out-param1 and you hope to capture the error data that was generated before the exception was thrown, you actually get a compilation error because e isn't in scope when you try to issue the message.

Not a big problem, of course, at least two work-around are obvious.

Except that I tried to send a message to our logger, so the code looked like try { ErrorInfo e; someRiskyOperation(e) } catch (...) { LOG_WARN("Risky operation failed with error " + e.explanation()); } where LOG_WARN is a macro whose purpose is to capture the file and line where the error occured.2 And that's a problem if this code occurs in a library that can be compiled with logging support turned off by simply defining the logging macros as nulls: #define LOG_WARN(explanation) , because in that case the compiler never sees the offending e.explanation. It was rewritten as nothing even before lexing. So you can test the code with logging turned off to your hearts content and never find the bug. Sigh.


1 Yes, I agree: out-params are generally evil. It's a legacy API. Perhaps we'll get around to replacing it eventually.

2 When it actually does something it looks something like #define LOG_WARN(explanation) do { \ logger:Message logmsg(__FILE__, __LINE__, explanation, logger::Warning); \ logger::getLogHandler().post(logmsg); \ while(false) and it can't be a function because __FILE__ and __LINE__ are macros and have to be evaluated in place. Unless and until the language provides operators for capturing source position data like that this is the best we can do.

2021-02-13

Little surprises #7: more Qt versus the preprocessor misery

Trying to be a good kid. Writing tests as I code. Testing the edge cases. "Hey, this should throw, does Qt have a test for that?" Yeah, its QVERIFY_EXCEPTION_THROWN. Great, let's use that!

void suspisiousFunctionCallThrows()
{
    // Define badInput and otherParam;
    
    QVERIFY_EXCEPTION_THROWN(suspicisouFunction(badInput,otherParam), std::runtime_error);
}

It doesn't compile. Why not? Commas again, of course.

And it is conceptually easy to fix: you just create some blind wrapper than make the offending call without taking multiple argument.

void suspiciousFunctionCallThrows()
{
    // Define badInput and otherParam;
    
    std::function f = [&]{
    	suspiciousFunction(badInput,otherParam);
    }

    QVERIFY_EXCEPTION_THROWN(f(), std::runtime_error);
}

Not exactly a featured stop on the "Look at our transparent tests" tour, is it?

2021-01-05

As if you needed another reason to hate the preprocessor

C++ inherited from c a compilation model that discards nearly all information about types and symbols. This is a mixed blessing. On one hand it simplifies a number of things and made c portable to a wide varienty of machines including some with minimal resources. On the other hand it means debuggers need added support to do their work and that reflection is not natively supported (in c) or requires special work (as in RTTI in c++). Unfortunately there are some very nice things you can do in the testing domain if you have reflection that are quite difficult without it.

This can be worked around in a number of ways, but most approaches make heavy use of the preprocessor: using function-like macros as well as the built in location macros (__FILE__, __LINE__ and their more modern counterparts). Indeed, your humble author once wrote a unit-testing framework for c and c++ in a combination of c-preprocessor and GNU make.1 More seriously Qt's native signals/slots mechanism and test framework make use of a variety of macros (Q_DECALRE_METATYPE, QCOMPARE, and so on).

Enter c++ templates. In particular templates taking more than one parameter. For example, say I'm using QTest and I want to perform a test of a computation that returns a three element stadard array of floats. I write something like QCOMPARE(actualArray, expectedArray);, compile (no problems) and run the tests which results in a hard crash in the bowels of qmetatype.h.

Oh yeah, I needed to declare the type to the metatype system.2

No problem. Scroll to the top of the file and add Q_DECLARE_METATYPE(std::array<float, 3>);, but that won't even compile (or if you're using Qt Creator the linter will catch it for you). Do you see why?

Q_DECALRE_METATTYPE is a macro. It's arguments are parsed as comma separated plain text. Which means the preproecssor reads that line as having two arguments where only one is expected. Crap.

To be sure you can work around this issue. My favorite approach is to use a type aliases like

using floatAry3 = std::array<float, 3>;
Q_DECLARE_METATYPE(floatAry3);

Keep in mind however, that this approach has its own trap: Qt's metatype system won't let you register the same type twice. The same underlying type, not the same name. Which means that you have to use a single name for each type to avoid inadvertent duplication, but you can't use the underlying typename for any multiple argument template types.

Anyway, the point is that you can't mix funciton-like macros with multi-argument templates without pain.


1 About all that can be said for it is I learned a lot and it worked well enough for me to use on some of my toy projects.

2 Not that you can tell from the crash or even from the stack trace in the debugger. All the behind the scenes magic that goes into QTest hides the origin of the error. I assume that most every QTest user has spent a frustring hour or so learning this the hard way.