[RFC] A compiler flag to enable experimental/unstable language and library features

Hi everyone,

In the past, libc++ has not been extremely good at conveying the experimental nature of some features to users or shipping TSes. For example, we shipped std::span to users before we (the developers back then) considered it stable, which caused some problems down the road. Similary, some vendors have not been shipping the c++experimental.a library (which contains the TSes that we implement). We didn’t have a good way for users to enable these experimental features while ensuring they wouldn’t start relying on (ABI or API) unstable features in production code, so it made little sense to ship them.

In addition to that, WG21 has recently been adding some large features like <ranges> and <format> to the Standard Library. These features are large and complex, and they take more than one release to implement and stabilize. However, they are often in a useful and interesting state even before we consider them fully stable, and we would like to have a way for users to try them out.

The goals of this proposal are:

  1. To make it possible for users to try experimental features in a simple and ergonomic way
  2. To make it difficult for users to accidentally start depending on unstable features in their code

The proposal

I’d like to propose adding a Clang flag (currently called -funstable) which enables the following things:

  1. Not fully implemented language features. For example, before lambdas were working to a satisfactory level or before we had pinned down their ABI, we could have guarded that language feature behind -funstable.
  2. Language TSes. For example, the Concepts TS could have been guarded behind such a flag before WG21 settled on the design we have now.
  3. Library TSes. For example, the Networking TS if we implemented it, or the std::pmr TS (which we do implement).
  4. Not-yet-complete library features. For example, <format> mostly works, but it is technically not complete yet and we would rather not commit to an ABI at this point. Currently, we simply don’t ship that feature, with no way of enabling it.
  5. Library features that we have implemented, but that we know are going to be broken by WG21 itself. For instance, WG21 made several API and ABI breaks to both <ranges> and <format>. Implementers were aware of those upcoming breaks, so we couldn’t have shipped <ranges> or <format> even if they were ready at the time.

This flag is basically meant for folks who want to play around with new features to see how they work and give feedback, and also to provide some sort of guarantee to vendors that users can’t start relying on them by mistake.

Other names that were considered

  • -fexperimental: We decided not to go with this because we wanted to convey the fact that said features were unstable, and hence dangerous to use in production code
  • -std=c++latest: This would be close to what MSVC does. We decided not to go down this route because we felt like it didn’t properly convey the notion of instability. Instead, we felt that -std=c++latest could/should be a simple alias for whatever the latest known Standard is (which is not part of this patch).

One potential rebutal I can think of would be folks saying we should unconditionally ship these features even if they are not fully baked yet. Unfortunately, this does not meet goal (2). Concretely, doing that would mean that vendors who care strongly about stability will not ship these features out of fear (founded or not, although in my experience it is founded), and hence that would also defeat goal (1).

I also want to acknowledge that this creates a potential trap of hiding every feature behind that flag and hence simply delaying feature delivery to users. In my opinion, this is something that must be handled by exercising proper judgement. We always ship features when they are good enough not to be harmful, on a best effort basis. This shouldn’t change, and we shouldn’t wait for something to be “perfect” before shipping it.

I would like to have something like this in place for LLVM 15, which is branching at the end of July. The proposed design has been partially implemented in ⚙ D120160 [Clang] Add `-funstable` flag to enable unstable and experimental features and ⚙ D121141 [Clang] Add `-fexperimental-library` flag to enable unstable and experimental features: follow-up fixes, but I would like to have a discussion here to converge on a design before we ship it officially.

Does anyone have thoughts?

Thanks,
Louis

4 Likes

I don’t really like the design of a unified -funstable. I don’t think it makes sense to opt-in to “everything unstable”; that’s just asking for trouble when we expand the set of unstable features. This is especially true if there’s any possible interaction with existing code. But even without that, it’s hard for someone looking at the command-line to figure out which unstable features the user actually wanted. If there’s some specific feature which we want to guard behind a flag, we can add a specific flag for it.

Maybe libcxx can use preprocessor macros? For example, something like -D_LIBCPP_EXPERIMENTAL_RANGES to enable using the <ranges> header.

In general I’m very much in favour of this. It makes a lot of sense for large features like ranges or format to be available, but guarded behind a flag. As I understand it, that is basically what clang does for modules or coroutines (with -fcxx-modules and -fcoroutines-ts respectively). It makes a lot of sense to have these kinds of flags for the standard library as well. A single flag for everything is probably enough. I don’t really expect anybody to be able use -fcxx-modules, but not be able to use some other incomplete feature. A single argument for all of those combined is probably a nice thing for users who want to play around with them. For small personal projects I actually recommend others to compile a ToT libc++ version to have all the fun features available to play around with. It’s not hard to do and a great way to learn about new language features, even before they are fully implemented.

However, I’d like to know more about how you would want to leverage the flag in libc++.
Would you want to use the unstable ABI with this flag enabled to ensure users don’t rely on the ABI being stable (like we plan to do for the debug mode)? In that case, should there be a -funstable-abi for users who don’t care about ABI stability but do care about API stability? If not, would you want to use the same ABI namespace as the stable ABI or set it to __unstable<LLVM_version> or something like that?
Would you like to introduce a _LIBCPP_HAS_NO_INCOMPLETE_<whatever> for all features which we have to implement over a period of time and remove that once the feature is almost complete to complete, even if that period is over a single release cycle?
Would -funstable be the default configuration for the CI? What is the rationale behind that decision?

These are implementation details, but IMO the answers to these questions are very important to determine what exactly you want to accomplish through this change.

1 Like

From a perspective of users, -funstable looks dangerous at the first sight. I must go to the docs to know what funstable contains. And we don’t know if it would grow in the future. So it becomes a burden to users.

It is different from -fcoroutines-ts or -fcxx-modules. We know what we are using and we know it is unstable. So I prefer the suggestion to use macros and the implementation could warn for the users who don’t suppress the warning by the specific macros.

Thanks for chiming in. Using macros doesn’t work because it can’t handle linking in the c++experimental.a library. It’s also not ergonomic and not discoverable for users. Finally, users shouldn’t be forced to enable individual library features explicitly, it’s more ergonomic to have a single switch that controls all of the experimental library features.

Following @efriedma-quic 's comments, it may be reasonable to back off mixing library and non-library features together with a single flag. Furthermore, following @philnik’s comments about ABI, it seems that perhaps -funstable isn’t the right name and instead something like -fexperimental-library might be better.

Would you want to use the unstable ABI with this flag enabled to ensure users don’t rely on the ABI being stable (like we plan to do for the debug mode)?

No, that’s not the plan. We want users to be able to link against a regular libc++.dylib shipped with a system when using -funstable (or -fexperimental-library).

In that case, should there be a -funstable-abi for users who don’t care about ABI stability but do care about API stability? If not, would you want to use the same ABI namespace as the stable ABI or set it to __unstable<LLVM_version> or something like that?

I think that is an interesting but separate question. Regarding the ABI namespace, we are already using __2 as the inline namespace in the unstable version (which is a shortcut for ABI v2 at this point).

Would you like to introduce a _LIBCPP_HAS_NO_INCOMPLETE_<whatever> for all features which we have to implement over a period of time and remove that once the feature is almost complete to complete, even if that period is over a single release cycle?

No, I wouldn’t add so much complexity. I would only guard really large features like <ranges>, <format> or something like a networking library if we have one in the future. You know, things that will take a lot of time to implement, yet could be useful to try out in the meantime.

Would -funstable be the default configuration for the CI? What is the rationale behind that decision?

Let’s call it -fexperimental-library instead to clear things up. On the one hand, I think it would be reasonable to make it the default in CI so that we test our incomplete and experimental features under as many configurations as possible. On the other hand, I think it would also make sense to ensure that the CI mirrors what we ship as closely as possible, so that would push towards not making it the default in CI. Either way, we’d want a CI job that tests the non-default case.

So here’s an updated proposal then:

We create a -fexperimental-library flag that doesn’t impact language-level features and only enables library TSes and incomplete features like <format> and <ranges>. That flag does not have any impact on the ABI namespace. It handles linking against c++experimental.a and setting any other macro that may be needed for experimental things to work.

How does that sound?

In that case, should there be a -funstable-abi for users who don’t care about ABI stability but do care about API stability? If not, would you want to use the same ABI namespace as the stable ABI or set it to __unstable<LLVM_version> or something like that?

I think that is an interesting but separate question. Regarding the ABI namespace, we are already using __2 as the inline namespace in the unstable version (which is a shortcut for ABI v2 at this point).

Sorry, it really wasn’t clear what my thought process on __unstable<LLVM_version> was. My idea was basically to have only the unstable parts in a different ABI namespace to try to break users who use it as part of their ABI. I wouldn’t want to block -fexperimental-features over that though, since we should be free to change that at any point later.

We create a -fexperimental-library flag that doesn’t impact language-level features and only enables library TSes and incomplete features like <format> and <ranges>. That flag does not have any impact on the ABI namespace. It handles linking against c++experimental.a and setting any other macro that may be needed for experimental things to work.

That might actually be a bit confusing. For example, <ranges> requires concepts, but they were guarded behind -fconcepts for a while IIRC. Why would I, as a user, have to set both -fexperimental-library and -fconcepts to get ranges? That’s really unintuitve IMO if -fexperimental-library is supposed to give me all of the unstable library features.

Thanks Louis!

I’m in favour of this proposal. (Full disclosure, I’m the main author of <format> in libc++.)
With our current approach vendors decide whether or not to enable these features and I expect all of them to use the default: not shipping these features. That means, that currently libc++ is the only Standard library implementation not shipping incomplete features. MSVC STL uses /std:c++latest and libstdc++ declares everything in C++20 as experimental.
With the proposed change the decision goes from the vendor to the user, which I think is good. The user knows their specific needs and requirements better than the vendor, who has to cater for the common good of all users.

I agree experimental sounds less scary than unstable so in retrospect I think the former is better.

I don’t mind having everything under one flag. When this flag contains “too much” we can add more flags. However items under this flag should become “mainline” by completing the feature/the TS gets adopted in the Standard.

I agree that would be confusing. However this can be solved in Clang by enabling g -fconcepts when -fexperimental-library is set. So it’s something we need to be aware of, but it’s easily caught in the CI.

The need for the static library and other bits of compiler awareness are a good reason to add a compiler flag.

My biggest concern is that we avoid breaking someone’s code by adding a feature the user didn’t intend to use in the first place. All sorts of flags tend to end up in build systems, and it’s easy to end up at a point where nobody really understands why specific flags are used.

If the flag only enables the use of new symbols from the C++ standard library, and doesn’t affect the usage of any existing functionality, then I think I’m okay with allowing all the “current” features without calling out specific features. I’m a little concerned that someone could end up accidentally pulling in functionality by ADL, but I guess that’s unlikely in most cases.

Reconsidering this, while I do think that @philnik has a point about e.g. Ranges needing concepts, I also think that enabling some experimental language features (e.g. modules) should be selectable by users. I can easily imagine someone wanting to compile with modules disabled but with experimental library features enabled. I’m not sure it would make sense to force using modules to these users.

So personally I’m converging towards a simple -fexperimental-library flag that only controls whether the library provides experimental features, leaving all the language-level experimental features to be controlled by other flags, like we do today.

Is that something that everyone would be happy with?

Looks like a good addition, nice!

+1 on the new name. Few questions:

  • How does -fexperimental-library behave in face of non-C++, is -std=c99 -fexperimental-library an invocation error?
  • Should this flag implicitly define a macro? Would be wise/desirable to do so?

I would say it’s not an error. If there are ever experimental C library features, they could be enabled by it, with any additional flag required to make it happen (such as linking against another library) being added by the driver implicitly.

In the current implementation, the flag can be sniffed by inspecting __has_feature(experimental_library), and I think that’s exactly what we want. In particular, defining a libc++ specific macro would tie the flag to libc++, which would be an artificial self-imposed limitation.

1 Like

I rather like to evaluate it on a case by case basis. For example the in the past both <ranges> and <format> used concepts, in that case I think it makes sense that -std=c++20 -fexperimental-library implies -fconcepts. When we don’t do that I expect a lot of bug reports of users who want to experiment with these new features.

On the other hand I would not expect -std=c++17 -fexperimental-library to set -std=c++20 to enable <ranges> and <format>.

I would say the rule of thumb should be: when during developing libc++ of a new experimental feature we need to enable an experimental compiler flags we add that flag to -fexperiential-library.

I consider modules in the same category as -std and should never be enabled by -fexperimental-library . This isn’t just a language feature but a completely different “language” in the same fashion that C++17 and C++20 are a different “language”.

I agree, but I think it can be quite hard to determine what the right choice is.

For example in this case it makes sense right now, but what would be the right thing when we start working on https://wg21.link/P2465 (which is tentatively ready for plenary)? I think the right thing would be to have -fexperimental-library imply -fcxx-modules, but could be overriden with -fno-cxx-modules for the people who don’t want it. For example -std=c++20 -fexperimental-library would imply -fconcepts and -std=c++2b -fexperimental-library would imply -fconcepts -fcxx-modules with the option to pass -fno-X to explicitly disable a feature and the experimental library parts that rely on it.

I think modules might be one of the hard choices. For example, I have no idea how clang-modules and C+±modules interact, or how much code gets broken when C+±modules are enabled. Therefore I think we should “evaluate it on a case by case basis”. In this case I first would like to see whether enabling

Some of the other clang options we had were a lot less controversial, concepts, coroutines, and char8_t come to mind.

1 Like

FWIW, for clang-cl environments, the linking aspect of the -fexperimental-library flag is a bit tricky, as in most clang-cl/msvc environments, you don’t call the compiler driver to do linking, but you call link.exe or lld-link directly. But I guess that’s solveable, by making the -fexperimental-library flag embed linker directives to pull in libc++experimental.lib into each object file? (For the regular library, the compiler itself doesn’t embed those directives, but they’re added by the libc++ header in llvm-project/__config at 61d417ceff90c4bd99bfed082d81bcc33ecf5a45 · llvm/llvm-project · GitHub)

We have to add some preprocessing for this anyways, so it should be possible to simply change the code to

#  ifndef _LIBCPP_NO_AUTO_LINK
#    if defined(_LIBCPP_ABI_MICROSOFT) && !defined(_LIBCPP_BUILDING_LIBRARY)
#      if !defined(_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS)
#        pragma comment(lib, "c++.lib")
#        if __has_feature(experimental_library)
#          pragma comment(lib, "c++experimental.lib")
#        endif
#      else
#        pragma comment(lib, "libc++.lib")
#        if __has_feature(experimental_library)
#          pragma comment(lib, "libc++experimental.lib")
#        endif
#      endif
#    endif // defined(_LIBCPP_ABI_MICROSOFT) && !defined(_LIBCPP_BUILDING_LIBRARY)
#  endif   // _LIBCPP_NO_AUTO_LINK

IIUC.

Yep, something along those lines would work (except that the library always is called libc++experimental.lib as there’s no shared version of it).

The fact that libc++experimental only is linked statically while linking the rest of libc++ dynamically is also the reason why libc++experimental doesn’t really work in shared linked clang-cl/msvc configurations anyway - because the headers would be indicating dllimport linkage for all types (via e.g. the _LIBCPP_TYPE_VIS and _LIBCPP_FUNC_VIS macros) even though the stuff in libc++experimental is linked statically and shouldn’t be accessed with dllimport linkage. Fixing that would require adding e.g. _LIBCPP_EXPERIMENTAL_*_VIS for all those functions/types/templates that are provided by the separate experimental library.

Maybe we should add a macro the next time someone puts something into libc++experimental.lib. It’s not that hard. I plan to work on std::pmr during LLVM 16, so it doesn’t make a lot of sense to change that right now. After removing experimental::pmr the library would be empty anyways.

Bulk replying – sorry I’ve been really busy with other stuff.

Regarding the Windows issue, I agree that we should use pragma comment(lib, "libc++experimental.lib") to make sure that the experimental features work on Windows just like they do on other platforms. I think we should also add appropriate visibility annotations to make it work.

I think the only contentious part at this point is whether -fexperimental-library should also turn on various experimental language features. When I started this proposal, my opinion was “yes”. However, it changed completely when I realized that some of those were undesirable, like modules. Similarly, I don’t think we should try to figure out what language features are required by any given library feature and then tweak the Clang driver to account for that. When we make Clang changes, we need to wait for roughly 1 year before we can assume those changes inside libc++ because of our support window.

Instead, I really think -fexperimental-library should exclusively control whether we provide experimental stuff in the library, and users can cherry-pick the other experimental language features they want (or need) to use.

I still have a preference to enable the needed compiler flags. However I see I’m in the minority. Instead I want to propose that instead of enabling the flags by default we validate whether the proper flags are set.

When an experimental header is included without -fexperimental-library all is fine. This can happen with indirect includes. We tried to make that an error in the past and that was reverted due to too many false positives.

When an experimental header is included with -fexperimental-library but without -ffoo required for that specific header then there should be an #error to explain which compiler flag or flags are required to use that header.

That way its easy for the user to set the proper flags.