Sorry for the long post. I have some trouble breaking it down into the essential aspects or find the right wording, and thus also googling it -- so please forgive me if this has been asked before. ;) I'll just describe the whole situation I'm facing here and try to be as complete as possible.
I'm currently tracking down a very weird bug where including the headers of some library X before my own classes leads to very strange compile time errors. The details are not that important here (I'll give a minimal example in a second!), but for context: I'm serializing my objects with a library called cereal
, and it suddenly tells me that my classes are not default-constructible anymore.
After cutting the included evil-stuff-breaking header into pieces I finally found out what happened, and recreated the bug in a reduced example, but I have no idea why things work (or don't) as they do, and maybe someone can explain this to me. :)
Some part of the included header of X
breaks a type trait in cereal
which determines if a given class T
can be default-constructed by cereal::access
.
So, first thing we need is the type trait. This is an implementation that is kind of similar to how the trait in cereal works (but it's not the same, extremely boiled down for the sake of a minimal example):
#include <type_traits>
namespace cereal {
using yes = std::true_type;
using no = std::false_type;
struct access {
template <class T>
struct construct {
T foo;
};
};
//! Determines whether the class T can be default constructed by cereal::access
template <class T>
struct is_default_constructible
{
template <class TT>
static auto test(int) -> decltype(cereal::access::construct<TT>(), yes());
template <class>
static no test(...);
static const bool value = std::is_same<decltype(test<T>(0)), yes>::value;
};
}
The basic idea is: If cereal::access:construct<T>
can be default-constructed (and therefore T
, too), the test(int)
method with yes = std::true_type
as return type is applicable and used to determine the static const bool value
, else the ellipsis version is used, which returns a no = std::false_type
.
I tested this first by appending the following code to the same file:
class HasDefault {
public:
HasDefault() = default;
};
class HasNoDefault {
public:
HasNoDefault() = delete;
};
class HasPrivateDefault {
private:
HasPrivateDefault() = default;
};
class HasPrivateDefaultAndFriendAccess {
private:
friend class cereal::access;
HasPrivateDefaultAndFriendAccess() = default;
};
#include <iostream>
int main(int, char**)
{
std::cout << "is it default constructible?" << std::endl;
std::cout << std::boolalpha;
std::cout
<< "HasDefault: "
<< cereal::is_default_constructible<HasDefault>::value
<< std::endl;
std::cout
<< "HasNoDefault: "
<< cereal::is_default_constructible<HasNoDefault>::value
<< std::endl;
std::cout
<< "HasPrivateDefault: "
<< cereal::is_default_constructible<HasPrivateDefault>::value
<< std::endl;
std::cout
<< "HasPrivateDefaultAndFriendAccess: "
<< cereal::is_default_constructible<HasPrivateDefaultAndFriendAccess>::value
<< std::endl;
return 0;
}
Which returns:
is it default constructible?
HasDefault: true
HasNoDefault: false
HasPrivateDefault: false
HasPrivateDefaultAndFriendAccess: true
Everything is fine so far.
BUT the library X
uses a similar approach to test if a given class has a member variable called Name
:
namespace somethingelse {
template <class T>
struct Whatever {
template <class TT> static std::true_type test(decltype(T::Name) *);
template <class TT> static std::false_type test(...);
static constexpr bool value =
std::is_same<decltype(test<T>(nullptr)), std::true_type>::value;
};
}
When I add this to the top of the file, all hell breaks loose. Or rather, everything compiles still fine, but the output of my program now changes to:
is it default constructible?
HasDefault: false
HasNoDefault: false
HasPrivateDefault: false
HasPrivateDefaultAndFriendAccess: false
Suddenly, the trait tells us that nothing is default-constructible... Hm!
In order to find out what is happening I altered some parts of the code and was able to find two possible fixes, which tell us a bit more about the problem.
The original functionality can be restored by either...
a) ... explicitly specifying the test method:
static const bool value = std::is_same<decltype(is_default_constructible::test<T>(0)), yes>::value;
or
b) ... renaming the somethingelse::Whatever::test
to e.g. somethingelse::Whatever::test1
.
Sadly, both parts are from different, external libraries. Due to option b) there is obviously the somethingelse::Whatever::test
chosen to get the value for cereal::is_default_constructible::value
. And this of course results in a std::false_type
, as my test classes do not have a Name
member variable. It is just the wrong test being used...
And this is where the title of this question comes from: For me, this is kind of a leak between different namespaces and even templated classes and methods. I mean, since Whatever
is templated, as well as Whatever::test
, with different template parameters, how the hell is it deducing to use that?
If I add something like
typeid(decltype(test<int>(0)));
to my main I get a compile error: Use of undeclared identifier 'test'
. Which is good. For cereal::is_default_constructible
it is not undeclared, as it knows test
from its own struct, but then again it actually accesses the something::Whatever<T>::test<TT>
... different namespace, different template, ...
So, I'm wondering: What the hell is going on here, why is it doing this? I'm probably just not aware of some cool c++ feature here that is just messing with me in this special case...
So... Please, enlighten me! :)
-- Nils
PS: Also, thanks for bearing with me and reading this wall of text! :)
PPS: I almost forgot some specs!
std=gnu++14
I've tried to further reduce the problem and ended up with this:
#include <type_traits>
namespace foo {
template <class T>
struct foobaz {
template <class U> static std::true_type test(U*);
static constexpr bool value = std::is_same<std::true_type, decltype(test<T>(nullptr))>::value;
};
}
namespace bar {
template <class T>
struct barbaz {
template <class U> static std::true_type test(int);
static constexpr bool value = std::is_same<std::true_type, decltype(test<T>(0))>::value;
};
}
int main()
{
bar::barbaz<int>::value;
}
Which results in a compiler error:
src/test.cpp: In instantiation of ‘constexpr const bool bar::barbaz<int>::value’:
src/test.cpp:27:23: required from here
src/test.cpp:9:84: error: no matching function for call to ‘bar::barbaz<int>::test<int>(std::nullptr_t)’
static constexpr bool value = std::is_same<std::true_type, decltype(test<T>(nullptr))>::value;
~~~~~~~^~~~~~~~~
src/test.cpp:18:50: note: candidate: template<class U> static std::true_type bar::barbaz<T>::test(int) [with U = U; T = int]
template <class U> static std::true_type test(int);
^~~~
src/test.cpp:18:50: note: template argument deduction/substitution failed:
src/test.cpp:9:84: note: cannot convert ‘nullptr’ (type ‘std::nullptr_t’) to type ‘int’
static constexpr bool value = std::is_same<std::true_type, decltype(test<T>(nullptr))>::value;
~~~~~~~^~~~~~~~~
So, it tries to instatiate constexpr const bool bar::barbaz<int>::value
by using the expression for constexpr const bool foo:foobaz<???>::value
.
Which makes me confident that @DanM is right and this is a compiler bug.
I've added a smaller example to my original question that gives compiler error with more information on what's going wrong. It seems like @DanM. is correct and this is just a compiler bug. Sadly, I wasn't able to find it on https://gcc.gnu.org/bugzilla
So, the answer is: Just use a different compiler / compiler version.
clang 6.0.0 and gcc 8.4.0 both worked for me.