When compiling following program with Xcode 10 GM:
#include <iostream>
#include <string>
#include <variant>
void hello(int) {
std::cout << "hello, int" << std::endl;
}
void hello(std::string const & msg) {
std::cout << "hello, " << msg << std::endl;
}
int main(int argc, const char * argv[]) {
// insert code here...
std::variant< int, std::string > var;
std::visit
(
[]( auto parameter )
{
hello( parameter );
},
var
);
return 0;
}
I get the following error:
main.cpp:27:5: Call to unavailable function 'visit': introduced in macOS 10.14
However, if I change min deployment target to macOS 10.14, the code compiles fine and it works, even though I am running macOS 10.13.
Since std::visit
is function template, and should not depend on OS version (which I proved by running the code on lower version of mac than actually supported), should this be considered as bug and reported to Apple or is this expected behaviour?
The same happens when compiling for iOS (iOS 12 is minimally expected).
All std::variant
functionality that might throw std::bad_variant_access
is marked as available starting with macOS 10.14 (and corresponding iOS, tvOS and watchOS) in the standard header files. This is because the virtual std::bad_variant_access::what()
method is not inline
and thus defined in the libc++.dylib
(provided by the OS).
There are several workarounds (all technically undefined behaviour), ordered by my personal preference:
std::visit
only throws if one of the variant arguments is valueless_by_exception
. Looking into the implementation gives you the clue to use the following workaround (assuming vs
is a parameter pack of variants):
if (... && !vs.valueless_by_exception() ) {
std::__variant_detail::__visitation::__variant::__visit_value(visitor, vs...);
} else {
// error handling
}
Con: Might break with future libc++ versions. Ugly interface.
Pro: The compiler will probably yell at you when it breaks and the workaround can be easily adapted. You can write a wrapper against the ugly interface.
Add _LIBCPP_DISABLE_AVAILABILITY
to the project setting Preprocessor Macros ( GCC_PREPROCESSOR_DEFINITIONS
)
Con: This will also suppress other availability guards (shared_mutex
, bad_optional_access
etc.).
It turns out that it already works in High Sierra, not only Mojave (I've tested down to 10.13.0).
In 10.12.6 and below you get the runtime error:
dyld: Symbol not found: __ZTISt18bad_variant_access
Referenced from: [...]/VariantAccess
Expected in: /usr/lib/libc++.1.dylib
in [...]/VariantAccess
Abort trap: 6
where the first line unmangles to _typeinfo for std::bad_variant_access
. This means the dynamic linker (dyld
) can't find the vtable pointing to the what()
method mentioned in the introduction.
Con: Only works on certain OS versions, you only get to know at startup time if it does not work.
Pro: Maintains original interface.
Add the following lines one of your project source files:
// Strongly undefined behaviour (violates one definition rule)
const char* std::bad_variant_access::what() const noexcept {
return "bad_variant_access";
}
I've tested this for a standalone binary on 10.10.0, 10.12.6, 10.13.0, 10.14.1 and my example code works even when causing a std::bad_variant_access
to be thrown, catching it by std::exception const& ex
, and calling the virtual ex.what()
.
Con: My assumption is that this trick will break when using RTTI or exception handling across binary boundaries (e.g. different shared object libraries). But this is only an assumption and that's why I put this workaround last: I have no idea when it will break and what the symptoms will be.
Pro: Maintains original interface. Will probably work on all OS versions.
This happens because std::visit
throws an bad_variant_access
exception in cases described here and since the implementation of that exception depends on an newer version of libc++ you are required to use versions of iOS and macOS that ship this new version (macOS 10.14 and iOS 12).
Thankfuly, there is a implementation path available for when c++ exceptions are turned off which doesn't depend on the newer libc++ so if possible you can use that option.
P.S. About the case where you increased the minimum deployment target to 10.14 and were still able to run the program normally on 10.13 I'm guessing you would run into problems at the point that this new exception would be triggered (since the exception method which relies on a newer version of libc++ would not be resolved).
Here's another alternative (that won't be palatable for some). If you're already using Boost, then you can use Boost.Variant2 when targeting iOS.
#if MACRO_TO_TEST_FOR_IOS_LT_11
#include <boost/variant2/variant.hpp>
namespace variant = boost::variant2;
#else
#include <variant>
namespace variant = std;
#endif
Then you can use variant::visit
in your code.
I'm still working out the kinks to test for the iOS target version (and if we're targeting iOS at all). That's why I used MACRO_TO_TEST_FOR_IOS_LT_11
above, as a placeholder.
Similarly you can also use abseil-cpp libraries to seamlessly use std::variant
where it is enabled, and abseil::variant
where it isn't.
Abseil is an open-source collection of C++ code designed to augment the C++ standard library.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With