24

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).

Cœur
  • 37,241
  • 25
  • 195
  • 267
cubiii
  • 359
  • 3
  • 11
  • Right now it sounds like expected behaviour to me. Your OS does not matter, the deployment target is the only thing that matters. For <10.14 the code does not compile, for >=10.14 it does. Or am I missing something? – luk2302 Sep 13 '18 at 09:42
  • 3
    But if I compile with deployment target iOS 12, the linker will also include other functions that are iOS 12-only and the binary will not work on iOS 8 anymore. I don't get it why this function, which is part of [C++17 standard](https://en.cppreference.com/w/cpp/utility/variant/visit) and should not depend on OS version, depends on macos 10.14 and ios 12? – DoDo Sep 13 '18 at 10:04
  • 1
    The `std::visit` is a function template, so it cannot be part of some dynamic library that is shipped with iOS 12 and macOS 10.14 only. As a function template, it is fully implemented in header and gets inlined in the calling code, so that shouldn't depend on any OS versions. – DoDo Sep 13 '18 at 10:07
  • @DoDo: That's a false assumption based on an oversimplification. At the _very_ least, permitting what you're proposing would be an absolute logistical nightmare. In reality, there is no benefit in supporting it. It is much better for there to be _one_ version of the implementation that you use, and to not care which parts may be built into the executable and which parts may be found in a runtime library. – Lightness Races in Orbit Dec 20 '18 at 12:49

5 Answers5

18

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:

1) Grab into the Implementation

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.

2) Suppress the Availability Compiler Error ...

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.).

2a) ... and just use it

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.

2b) ... and provide your own exception implemention

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.

Tobi
  • 2,591
  • 15
  • 34
14

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).

m1h4
  • 1,139
  • 2
  • 13
  • 22
2

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.

A. K.
  • 34,395
  • 15
  • 52
  • 89
jakar
  • 1,031
  • 1
  • 11
  • 22
0

Add CXXFLAGS += -D_LIBCPP_DISABLE_AVAILABILITY to your Makefile. See some of the other posts to see details on pros and cons of this, but this will get the code to compile and run.

-1

Even though templates generally come from headers, doesn't mean the runtime target doesn't matter. Those templates are part of a wider library, and they compile to code that must still be compatible with the rest of that library. It makes sense for the whole standard library to be of one, single version, and it makes sense for that version to be the one that'll work on the target machine. Can you imagine the chaos that would ensue otherwise?

Some of the others here have given some low-level, practical reasons why in this particular case that version unity is important. Personally I think it's best to forget about implementation details like "templates go in headers" in situations like this; you shouldn't need to care about it, plus you risk making abstraction-breaking assumptions for little benefit. Just code to contract and you'll be fine.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • Yeah, in an ideal world I would agree with you. But coding to contract would mean basically not being able to use modern C++ if I wish for my app to support older versions of OS. And having support only for latest, most modern OS version, is simply not possible for commercial apps. – DoDo Dec 21 '18 at 12:40
  • @DoDo Indeed, if you're targeting older OSes then you have to use older C++. But that's okay. Until very recently, for me, C++11 was a luxury! That's just how it is. My answer is about the real world not an ideal world :P – Lightness Races in Orbit Dec 21 '18 at 12:59
  • I strongly disagree. I think that it is not OK to be forced to use older version of C++ if you wish to support older operating systems. This is simply not in the nature of C++. And, as mentioned by other people here, it is possible to use C++17 features on older systems, if you know what you are doing. – DoDo Dec 24 '18 at 16:43
  • @DoDo It's definitely in the nature of C++; that's why there are distinct versions of it, and why all major compilers have versions switches. Certainly you can upgrade in some cases (I used devtoolset to get GCC 4.8 and C++11 on CentOS 6). But, particularly in Enterprise, you are interacting with other parts of the ecosystem you're deploying to, and version management is a crucial part of our job accordingly. Insisting on using "whatever was just invented" causes a massive headache for people around you. Of course none of this actually changes the facts of the matter. Happy Christmas! – Lightness Races in Orbit Dec 24 '18 at 16:47
  • Unfortunately, choosing an earlier version of C++ standard will not make your program compatible with earlier versions of OS. For example, even if you use C++11, instead of C++17, but are compiling your code with GCC 8.2 on ArchLinux, it will not work on CentOS 6, due to GLIBC requirements. I was just stating that header only STL containers should not have a dependency on particular OS version, and they actually don't have - the error here was just due to the fact that Apple decided to ship libc++ with latest versions only. If you link it statically, it will work. Merry Christmas to you too! – DoDo Dec 25 '18 at 18:48
  • @DoDo Of course you can't just change a compiler switch to get an earlier language version and magically make your program compatible with older computers. That's not what anybody was claiming. What you do is to use older compilers, and when you use older compilers, there is a chance that you will be constrained to using older versions of the language. This doesn't change whether you use some library feature that happens to be fully implemented in a header file or not. Code to the contract of the implementation you're using, period, full stop. – Lightness Races in Orbit Dec 25 '18 at 23:31