1

In my project I am using Boost.Bimap to implement bidirectional maps.

Look at this very simple MCVE on godbolt, where I am using structured binding to print the key-value pair of the right map (which, according to the documentation, is signature-compatible to std::map.

Problem

It compiles fine for any g++ version >= 7.4 and later, however I need to use g++ 7.1. and here this code fails with the following message:

<source>: In function 'int main()':

<source>:11:20: error: 'std::tuple_size<const boost::bimaps::relation::structured_pair<boost::bimaps::tags::tagged<const long unsigned int, boost::bimaps::relation::member_at::right>, boost::bimaps::tags::tagged<const std::__cxx11::basic_string<char>, boost::bimaps::relation::member_at::left>, mpl_::na, boost::bimaps::relation::mirror_layout>>::value' is not an integral constant expression

   for (const auto& [key, value] : bm.right) {

I was able to find out that this is due to a bug in g++ that seems to have been fixed in later versions.

Workaround attempt (toy example, successful)

In order to make the structured bindings work with my compiler version, I attempted to create a workaround by specializing std::tuple_size, std::tuple_element and std::get. See this cppreference link for more information.

For simplicity, I successfully tried this first with a toy structure. Here are the specializations, check out the full code on godbolt.org:

struct SampleType {
  int a = 42;
  std::string b = "foo"s;
  double c = 3.141;
};

#if (__GNUC__ == 7) && (__GNUC_MINOR__ == 1)
  template <std::size_t N>
  decltype(auto) get(const ::SampleType& t) {
    if      constexpr (N==0) return t.a;
    else if constexpr (N==1) return t.b;
    else                     return t.c;
  }

  namespace std {
    // Tuple size is 3
    template <> struct tuple_size<::SampleType> : std::integral_constant<std::size_t, 3> {};

    // Define tuple types
    template <std::size_t N> struct tuple_element<N, ::SampleType> {
        // Deduce type from get() function template defined above
        using type = decltype(::get<N>(std::declval<::SampleType>()));
    };
  }
#endif

Note that if you remove the #ifdef for g++ 7.1., the compilation will fail with the same error as above (...is not an integral constant expression). (Interesting: Unlike the boost::bimap example, which only compiles fine with g++ 7.4 onwards, the toy example already succeeds with g++ 7.2)

Workaround attempt (original example, not successful)

Now, being very convinced that I found the solution, I made an attempt to do the same for boost::bimap but I am failing helplessly (check it out on godbolt.org):

template <std::size_t N>
decltype(auto) get(const bimap::right_map::value_type& bm) {
  if      constexpr (N==0) return bm.first;
  else if constexpr (N==1) return bm.second;
}

namespace std {
  // Tuple size is 2 -> key-value pair
  template <> struct tuple_size<bimap::right_map::value_type> : std::integral_constant<std::size_t, 2> {};

  // Define tuple types
  template <> struct tuple_element<0, bimap::right_map::value_type> { using type = std::string; };
  template <> struct tuple_element<1, bimap::right_map::value_type> { using type = std::size_t; };
}

The error message is too long to post here (see godbolt output), but basically I understand that the overload for "my" get is not being matched by the compiler. Note that for debugging reasons I have inserted the following line into my code to make sure that I am actually dealing with the correct type in my specializations.

for (const auto& pair : bm.right) {
  // Make sure we capture the right type in the specializations above
  static_assert(std::is_same<
      decltype(pair),
      const bimap::right_map::value_type&
  >::value);
}

Am I doing something wrong? Or is this bug posing an insurmountable obstactle to my workaround attempt?

andreee
  • 4,459
  • 22
  • 42

1 Answers1

1

I don't think this is something you can work around.

Here's a shorter reproduction:

#include <tuple>

namespace N {
    struct X {
        template <typename T> void get() { }
    };
}

namespace std {
    template <> struct tuple_size<N::X> : integral_constant<size_t, 1> { };
    template <> struct tuple_element<0, N::X> { using type = int; };
}

namespace N {
    template <size_t I> decltype(auto) get(X const&) { return 42; }
}

int main() {
    auto [i] = N::X{};
}

This is a valid program. The wording from [dcl.struct.bind]/4 says, emphasis mine:

The unqualified-id get is looked up in the scope of E by class member access lookup ([basic.lookup.classref]), and if that finds at least one declaration that is a function template whose first template parameter is a non-type parameter, the initializer is e.get<i>(). Otherwise, the initializer is get<i>(e), where get is looked up in the associated namespaces ([basic.lookup.argdep]).

The fact that N::X has a member function template get() that takes a type template parameter should cause us to then consider ADL lookup on get, which should find the non-member N::get. gcc 7.4 does this correctly, gcc 7.3 complains about N::X::get() not working.


The only way to work around that is to wrap the initializer somehow. Basically do something like:

auto [i] = wrap(N::X{});

Where wrap returns some new type that definitely doesn't have a member named get, so that you can provide the non-member you want. I'm not sure if there's a solution here that doesn't require additional wrapping. Besides just using gcc 7.4 :-)

andreee
  • 4,459
  • 22
  • 42
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Very interesting. It seems like the ADL lookup for this case is completely broken in gcc 7.1. `N::X::get()` doesn't even need to be a function template, `void get() { }` fails to compile similarly. In fact, [_any_ defintion with name `get` leads to an compile error...](https://godbolt.org/z/OkxeGd). – andreee May 08 '19 at 08:37
  • @andreee It's not ADL that's broken, it's the first lookup - we should back out and try ADL if it fails, but we don't. – Barry May 08 '19 at 12:02