More thoughts on the [cpp.no_flag_forking] rule

Please see the main previous Discourse discussion on this topic here (as well as other discussions on various GitHub issues).

I’ve looked through various flag forking approaches and I want to contrast them and propose a slight change to the status quo. The approaches I’ve looked at include:

  • The status quo (example: iterator_interface)
  • The status quo with header-only fallbacks (example: inplace_vector)
  • Allowing flag forking unconditionally (Vito’s preferred option)
  • The “tweak header” approach proposed by pseudonymous blogger “vector-of-bool” (blog post)

Here’s my take. I don’t want to abandon the prohibition on flag forking entirely. The idea of recording the set of alternatives that was chosen for a particular build and then being able to rely on their availability to prevent ODR issues is too appealing to me.

The fact that the packager’s platform may differ from the consumer’s platform causes ODR problems if flag forking is allowed. If the packager creates an object file with one set of chosen alternatives that’s used by a consumer whose headers choose different alternatives than the object file the consumer is linking to, there is an ODR violation.

The consumer can work around this problem either by ensuring that the packager is using the same platform as the consumer, or by configuring the packager to choose the alternatives that are appropriate for the consumer’s platform rather than the packager’s.

(I’m also ruling out the vector-of-bool approach because it also doesn’t address the ODR problem, as the blog post itself acknowledges: “For any build that relies on stable, immovable, never-changing external binaries, tweak-headers will not work on those binaries.”)

Having said that, I also think that the status quo as demonstrated in iterator_interface is overly strict. Although we may wish that this wasn’t the case, I think that we still live in a world where we can significantly increase the number of developers who are willing to try our libraries out by supporting copy-paste vendoring as a first class approach, as is done by inplace_vector by wrapping the #include of config.hpp in __has_include and providing defaults if it isn’t present. To my mind this gets us the best of both worlds.

I do, however, want to make a slight change to inplace_vector. Currently inplace_vector puts the following boilerplate in inplace_vector.hpp:

#if !defined(__has_include) || __has_include(<beman/inplace_vector/config.hpp>)
#include <beman/inplace_vector/config.hpp>
#endif

// ...

#ifndef BEMAN_INPLACE_VECTOR_NO_EXCEPTIONS
#define BEMAN_INPLACE_VECTOR_NO_EXCEPTIONS() 0
#endif

This works fine for inplace_vector.hpp since it only has the one header file, but scales poorly for libraries with more than one header.

I would like to rename the generated config.hpp to config_generated.hpp, which then allows us to situate the boilerplate in a separate, non-generated config.hpp file that all headers can include:

#ifndef BEMAN_INPLACE_VECTOR_CONFIG_HPP
#define BEMAN_INPLACE_VECTOR_CONFIG_HPP

#if !defined(__has_include) || __has_include(<beman/inplace_vector/config_generated.hpp>)
#include <beman/inplace_vector/config_generated.hpp>
#else
#define BEMAN_INPLACE_VECTOR_NO_EXCEPTIONS() 0
#endif

#endif

I’ve created a draft pull request here.

I want to update the Beman standard to allow the __has_include approach above and recommend it as the preferred way to do this. I also want to explicitly whitelist flag forking on the presence or absence of _MSC_VER, since there’s no risk of ODR violations caused by two artifacts being linked together from completely different ABIs.