3

Let's say I have the following code:

// exposition-only, I can't use this macro in my solution
#if defined(HAS_MY_ENUM)
enum my_enum {
    zero,
    one,
};
#endif

enum empty {
    placeholder, // an enum must always have a value
};

I'd love to have a conditional type alias my_enum_or_empty that is set to my_enum if it is defined, otherwise it's set to empty, e.g.:

using my_enum_or_empty = std::conditional<my_enum_exists, my_enum, empty>;

I want a SFINAE-based solution that gives me my_enum_exists.

I was thinking of this approach, but it requires a forward-declaration of my_enum, and it cannot be forward declared, because it is sizeless. And I cannot add a size, because this enum comes from an external library. Do you have any ideas?

Restrictions

My use case is that this my_enum is defined in an external C library that I don't have control over. I need to compile my code against two versions of this library (with my_enum and without). The header sources, where my_enum is defined cannot be changed i.e. it must be an old C-style enum.

The library version is not known at compile time. Sadly, there is no LIBRARY_VERSION macro. I need to rely on this enum alone.

I cannot rely on the build system. In fact, I am doing a C++ header-only wrapper around this C code, hence there is no build system. Neither can I move responsibility to my customer. There are more than one type missing and there are many versions of this library, i.e. it'd be too complex for my wrapper user.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
ivaigult
  • 6,198
  • 5
  • 38
  • 66
  • 3
    Why not continue to use `#if defined(HAS_MY_ENUM)` ... ? – Richard Critten Jun 12 '23 at 17:10
  • @RichardCritten good point, I edited the post!:) – ivaigult Jun 12 '23 at 17:12
  • 1
    @ivaigult what _do_ you have in the header to cause the conditional definition? – JohnFilleau Jun 12 '23 at 17:15
  • I have two versions of the _external_ library: old and new. One has the enum, one does not. My code should work for both versions. – ivaigult Jun 12 '23 at 17:16
  • What is the problem you are trying to solve? If you have a naming conflict use namespaces otherwise more information would be useful. – Pepijn Kramer Jun 12 '23 at 17:17
  • I cannot refactor the external C library to use namespaces, I'm afraid:) – ivaigult Jun 12 '23 at 17:18
  • I would love a way to handle this, but honestly I think you need some kind of version check on the library. – JohnFilleau Jun 12 '23 at 17:18
  • 1
    Is the library version known at compile time? Or do you need to do some sort of runtime magic? – JohnFilleau Jun 12 '23 at 17:19
  • What I usually do is create a C++ abstract baseclass with methods modeling what I would like to do in the 3rd party library. (with my own types etc). Then make an implementation using the 3rd party libary and inject the interface into the C++ code (dependency injection). That way I can also inject a mock for unit testing (a win-win in my book). The good thing is you can do the implementation in a static C++ library which links the "C" library (thus hiding the "C" library as an implementation detail) – Pepijn Kramer Jun 12 '23 at 17:22
  • When compiling against old library `clang++ -DNO_MY_ENUM foo.cpp`, when compiling against new library `clang++ -DHAS_MY_ENUM foo.cpp`. – Eljay Jun 12 '23 at 17:22
  • Your build system knows which library version it's building against at build time, right? Do what @Eljay said above. If the library doesn't expose a compile time definition, then define your own. `-DMY_LIBRARY_USING_THIRD_PARTY_LIBRARY_VERSION_ONE` etc – JohnFilleau Jun 12 '23 at 17:23
  • Also, I realize this is a stereotypical stackoverflow response, but: "Don't build against both 3PL versions. Just build against one." – JohnFilleau Jun 12 '23 at 17:24
  • So many questions here, thank you! I edited the notes section to better explain my use case. – ivaigult Jun 12 '23 at 17:27
  • 1
    If there's no build system (you're distributing this to customers), then move the build-system-compile-time-definition responsibility on to them. "If you're using my wrapper, you MUST specific which version of 3PL you're building against. Do this by defining one of the following compile definitions..." – JohnFilleau Jun 12 '23 at 17:27
  • I get the desire to automate all of this away, but dependency management is HARD. Sometimes the best thing to do is tell customers that they have to do some extra leg work. For example, libcurl requires that any customers building against OpenSSL do extra work depending on the version of OpenSSL they are linking against. https://curl.se/libcurl/c/threadsafe.html – JohnFilleau Jun 12 '23 at 17:30
  • 1
    Best of luck. "I *need* a SFINAE solution" should be written "I *want* a SFINAE solution". If you figure it out, please add an answer here, because it's an interesting problem. – JohnFilleau Jun 12 '23 at 17:32
  • Related answer because it validates my point of view (therefore it is 100% correct and I will die on this hill) :P https://stackoverflow.com/a/53298232/2027196 – JohnFilleau Jun 12 '23 at 17:37
  • For SFINAE to work the compiler still needs to know what it's dealing with. It's unsuitable for this kind of test. Many libs will define version macros though, which could be used to conditionally use different options. The only other option here would be to have your build system do a test compilation and decide based on the exit code of the compiler, which alternative should be used. One example is for this functionality is [cmake's `try_compile`](https://cmake.org/cmake/help/latest/command/try_compile.html) – fabian Jun 12 '23 at 17:39
  • Are there any other "proxy measurements" you can make about the 3PL at compile time to determine what version it is? Maybe there's a public struct that got a new member variable between versions A and B? Checking for the presence of a member variable is something you can do with SFINAE. I'd expand this question to "how can I proxy-measure the version of a library?" for your own sanity. – JohnFilleau Jun 12 '23 at 17:40
  • 2
    This can't be done with SFINAE, because it can only be used to examine template parameters, and there's nothing about a global enum that can serve as one. [`requires` has the same issue.](http://eel.is/c++draft/expr.prim.req.general#note-1) What is this library called and what enum are you checking for? – HolyBlackCat Jun 12 '23 at 17:43
  • @HolyBlackCat with the practical question. Yeah this is beyond generalities. There might be a perfectly reasonable way to handle this given the specific library. – JohnFilleau Jun 12 '23 at 17:43
  • Also, contact the library maintainers and tell them to add a version macro to the next library iteration. – JohnFilleau Jun 12 '23 at 17:45
  • May be not exactly sfinae, I need something similar to [this trick](https://stackoverflow.com/questions/48257767/select-global-scope-function-at-compile-time/48264444#48264444) with function fallback. – ivaigult Jun 12 '23 at 17:49
  • @ivaigult can you say what 3PL this is and which enum you're checking for? – JohnFilleau Jun 12 '23 at 17:55
  • Ok. Can you add the library name and the enum name? – HolyBlackCat Jun 12 '23 at 18:00
  • 1
    Sorry, it's complicated: the headers are proprietary and licensed, so I cannot share:-( I can only says it describes some hardware. There is a library version, but changes between them are unmanageable. For my particular application, I need fallback types for ~100 types. – ivaigult Jun 12 '23 at 18:04
  • 1
    I see. You should probably mention it in the question. Also, use `@username` when replying to comments, otherwise we don't get notifications. – HolyBlackCat Jun 12 '23 at 18:13
  • Can I just say, your entire situation is a case of "been there, done that". When you say fallback types for ~100 types, there are still only 2 library versions right? – JohnFilleau Jun 12 '23 at 18:25
  • @ivaigult can you answer my question about whether there are any proxy measurements available? E.g., if there's a struct that has changed between versions, that's a gold mine for what you want. – JohnFilleau Jun 12 '23 at 18:26
  • @JanSchultke I don't think I can use `std::conditional`, bc when `my_enum` is not defined, this will fail to compile. I think the semantic has to be `using my_enum_or_empy = ...`. – ivaigult Jun 12 '23 at 18:34
  • 1
    @JohnFilleau I understand it's is possible to use a solution that depends on the library version (or a structure's `sizeof`). The problem is that there are too many versions / types and that would be really hard to implement. Where as with with `my_${typename}_or_emtpy` semantics I could just list the super set of all types. – ivaigult Jun 12 '23 at 18:36

2 Answers2

8

@Artyer suggests a more practical approach in his answer. I'm leaving this here mostly as a technical curiosity.


What you can do, if you know the name of the one of the constants in the enum, is to define it as a macro (assuming the target header doesn't use that name anywhere else), which would expand to itself plus some magic to detect whether it was used or not (stateful metaprogramming).

The following works with the assumption that the constant doesn't have a = value specified. If it does and you know the exact value beforehand, supporting that case should be trivial. If it does but you don't know the value, it might still be possible, but will be somewhat more ugly.

#include <type_traits>

namespace EnumDetector
{
    namespace detail
    {
        constexpr void _adl_EnumMarker() {} // Dummy ADL target.

        template <typename T>
        struct Reader
        {
            #if defined(__GNUC__) && !defined(__clang__)
            #pragma GCC diagnostic push
            #pragma GCC diagnostic ignored "-Wnon-template-friend"
            #endif
            friend constexpr auto _adl_EnumMarker(Reader<T>);
            #if defined(__GNUC__) && !defined(__clang__)
            #pragma GCC diagnostic pop
            #endif
        };

        template <typename T>
        struct Writer
        {
            friend constexpr auto _adl_EnumMarker(Reader<T>) {return true;}
        };

        template <typename T, typename = void> struct Value : std::false_type {};
        template <typename T> struct Value<T, std::enable_if_t<_adl_EnumMarker(Reader<T>{})>> : std::true_type {};
    }

    template <typename T>
    inline constexpr bool HaveEnum = detail::Value<T>::value;
}

#define ENUM_DETECTOR_REGISTER_WITHOUT_VALUE(tag_, enumerator_) \
    DETAIL_ENUM_DETECTOR_REGISTER_WITHOUT_VALUE(__COUNTER__, tag_, enumerator_)

#define DETAIL_ENUM_DETECTOR_REGISTER_WITHOUT_VALUE(counter_, tag_, enumerator_) \
    DETAIL_ENUM_DETECTOR_CAT(_enum_detector_helper_,counter_), \
    DETAIL_ENUM_DETECTOR_CAT(_enum_detector_helper2_,counter_) = (void(::EnumDetector::detail::Writer<tag_>{}), 0), \
    enumerator_ = DETAIL_ENUM_DETECTOR_CAT(_enum_detector_helper_,counter_)

#define DETAIL_ENUM_DETECTOR_CAT(x, y) DETAIL_ENUM_DETECTOR_CAT_(x, y)
#define DETAIL_ENUM_DETECTOR_CAT_(x, y) x##y

// ---
// Repeat following for all enums:

struct MyEnumTag {};

#define my_enum_constant ENUM_DETECTOR_REGISTER_WITHOUT_VALUE(MyEnumTag, my_enum_constant)

// ---

#ifdef YOUR_LIB_INCLUDE_GUARD // <-- Customize this for the target library, or remove altogether if it uses `#pragma once`.
#error "Must not include this stuff elsewhere.."
#endif

// #include <stuff.h>
enum SomeEnum { x, y, my_enum_constant, z, w }; // Try commenting me out to trigger the assertion.

// ---
// Repeat following for all enums:

#undef my_enum_constant

// ---
// Lastly, the usage:
static_assert(EnumDetector::HaveEnum<MyEnumTag>);
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
6

How about something like this:

template<typename T = int>
constexpr bool my_enum_exists = requires { my_enum(T{}); };

(Which can be implemented without the requires, you just need to SFINAE on the expression my_enum(T{})).

When my_enum is defined as a type, this will just check if you can do the functional cast my_enum(0), which you can, so it passes.

When it's not defined, this will check if you can call a function called my_enum with T{} (via ADL, so you don't need a dummy function called my_enum), which will always fail for non-class type int.


Taking advantage of this "adl or type cast", you can make a struct that can find functions by ADL returning your empty type if the enum name can't be found. This can easily be expanded for multiple types:

enum empty {
    placeholder, // an enum must always have a value
};
struct enum_existance_checker {
    // Convert to enum type or int argument type
    template<typename T> operator T();

    friend empty my_enum(int);
    friend empty my_other_enum(int);
    friend empty my_third_enum(int);
};

enum my_enum {};
// enum my_other_enum {};
enum my_third_enum {};

// This is a cast to my_enum, using the conversion operator.
using my_enum_or_empty = decltype(my_enum(enum_existance_checker{}));
static_assert(std::is_same_v<my_enum_or_empty, my_enum>);

// This is a call to the friend function, converting to int
using my_other_enum_or_empty = decltype(my_other_enum(enum_existance_checker{}));
static_assert(std::is_same_v<my_other_enum_or_empty, empty>);
Artyer
  • 31,034
  • 3
  • 47
  • 75