GameSalutes
GameSalutes

Reputation: 1870

std::is_default_constructible with private access and friend function

I was surprised to discover that std::is_default_constructible appears to ignore friend access. When declaring a default constructor private in a class and then friending a function, I'd expect that std::is_default_constructible would return true.

Example: I ran the following on Wandbox: https://wandbox.org/ using Clang 5.0.0 and GCC 7.2.0 under C++17.

#include <type_traits>
#include <cassert>

class PrivateConstructor
{
    private:
        PrivateConstructor() = default;
        friend void doIt();

};

void doIt()
{
        bool isConstructible = std::is_default_constructible<PrivateConstructor>::value;
        PrivateConstructor value;
        assert(isConstructible); // FAILS!
}
int main(int,char**)
{
    doIt();
    return 0;
}

This code compiles but the assertion fails. Is the defined explicitly in the standard or is this a possible compiler bug?

Upvotes: 2

Views: 927

Answers (4)

Adam Badura
Adam Badura

Reputation: 5339

Depending on your exact context and your need you might be able to get away with something like this (with T being the type in question):

class DerivedT : public T {};
static_assert(std::is_default_constructible_v<DerivedT>);

If T has a default constructor that is protected (or public, of course) the DerivedT will be default constructible and static_assert will pass.

The private case requires even more "trickery". If the default constructor is private but you want to know if it exists, I presume you may have some other means to invoke it somehow (a friend?), and perhaps similar means should be applied to DerivedT to simulate this as well.

However, the private case is also riskier. I would expect some legacy code (when = delete wasn't available) may be using private default constructor (preferably also without defining it - only declaration) to actually prevent default construction and make a clear statement of it (rather than relying on the compiler disallowing it implicitly).

Upvotes: 0

Don Pedro
Don Pedro

Reputation: 343

I think there are several points to summarize:

  • The comment from S.M. suggests that the standard seems to move towards binding std::is_default_constructible to the public interface of some class. It means that std::is_default_constructible will yield false for a class with a private or protected default constructor. This has a reason.
  • Several answers suggest to befriend std::is_default_constructible in order to get the desired true value. This is wrong and should be avoided, explanation follows.
  • Regularly, asking for the presence of a non-public default constructor is asking the wrong question or is even a non-sensical question! Consider:
    • You may reasonably ask for global constructability (via std::is_default_constructible), giving you the information if anybody can build a specific object (due to exposing a public constructor).
    • But if asking if class has a private constructor, what information will it give to you? That not everybody can construct such an object? What worth would it have to you and what would you SFINAE on this? You don't have the information specifically who can and who cannot build this object, so knowing about a private constructor doesn't answer anything.
    • The question you should've asked instead is if your specific class targeted to instanciate this specific object can construct this object, right? So you need to SFINAE inside the friend class/function, not globally.
  • The comment of Jarod42 gives this link: Disallowing the friending of names in namespace std. Why is this? Because what's being done in the implementation of some functionality is absolutely implementation defined. So making std::is_default_constructible a friend of some class may yield the expected result on compiler A but will fail on B. Or it may fail with the next release of A. You don't want to rely on that, do you?

GameSalute tested with Clang and GCC. To show you a bit more what this can lead to I've done some experiments with MSVC, using Visual Studio 2017 V15.9.11 (currently the most recent version) in C++14 mode:

#include <memory>
#include <type_traits>

class NoFriend
{
private:
    NoFriend() = default;
};

class Friended
{
    friend struct std::is_default_constructible<Friended>;

    //friend constexpr bool std::is_default_constructible_v;                                // C2433: 'std::is_default_constructible_v': 'friend' not permitted on data declarations
                                                                                            // C2063: 'std::is_default_constructible_v': not a function

    //friend constexpr bool std::is_default_constructible_v = __is_constructible(Friended); // C2433: 'std::is_default_constructible_v': 'friend' not permitted on data declarations
                                                                                            // C1903: INTERNAL COMPILER ERROR in 'C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\VC\Tools\MSVC\14.16.27023\bin\HostX64\x86\CL.exe'

    //friend bool __is_constructible(Friended);                                             // Non portable attempt:
                                                                                            // C2059: syntax error: '__is_constructible'
                                                                                            // C2238: unexpected token(s) preceding ';'

private:
    Friended() = default;
};

class Tester
{
public:
    void TestNoFriend() const
    {
        constexpr bool my_is_default_constructible_v = std::is_default_constructible<NoFriend>::value;

        static_assert(std::is_default_constructible<NoFriend>::value == false, "NoFriend is default constructible");
        static_assert(std::is_default_constructible_v<NoFriend>      == false, "NoFriend is default constructible");

        static_assert(my_is_default_constructible_v                  == false, "NoFriend is default constructible");
    }

    void TestFriended() const
    {
        constexpr bool my_is_default_constructible_v = std::is_default_constructible<Friended>::value;

        static_assert(std::is_default_constructible<Friended>::value == true, "Friended is not default constructible");
        //static_assert(std::is_default_constructible_v<Friended>      == true, "Friended is not default constructible"); // C2338

        static_assert(my_is_default_constructible_v                  == true, "Friended is not default constructible");
    }
};

What you can see from this snippet:

  • The standard suggests a specific implementation (std::is_default_constructible_v) but MS uses some compiler intrinsic to implement std::is_default_constructible and std::is_default_constructible_v: __is_constructible(Type). This is causing below mentioned deviating behavior.
  • MS does evaluate the friending of the classes, so it's giving a different result than Clang and GCC.
  • In MS' current incarnation std::is_default_constructible::value gives a different result(!!) than std::is_default_constructible_v. This is additionally fatal if you naively SFINAE on the outcome of the test!
  • In the commented out code I tried some (of course rather nonsensical) ways of friending std::is_default_constructible_v. Some expectedly fail (C2433, C2063, C2059, C2238) but one is even causing an internal compiler error (C1903)!

So all I can recommend is to in fact stay right away from friending in namespace std. All that's being based on such a construct is non portable and in today's compilers also partially broken and inconsistent (at least for MS). Anyway, as asking for a private default constructor is rather nonsensical (except maybe if you're implementing a super-smart boost serializer class that needs non-intrusive access to private members) this is not a big problem. Instead ask the right questions and SFINAE on them.

Upvotes: 0

Jarod42
Jarod42

Reputation: 217810

std::is_default_constructible<PrivateConstructor>::value has to return false.

Giving friendship to that class doesn't guaranty to change the result (implementation might rely on other class).

There is even a proposal (p1339r0) to make it clear that we should not give friendship to std class (unless otherwise permitted by the standard).

Upvotes: 2

3CxEZiVlQ
3CxEZiVlQ

Reputation: 38773

You declared the function doIt() to be friend of the class, but that function does not access the private class members. Instead, the function std::is_deafault_constructible accesses the class members.

template< class T >
struct is_default_constructible : std::is_constructible<T> {};

The proper way is declaring std::is_default_constructible to be a friend class:

friend class is_default_constructible<PrivateConstructor>;

Upvotes: 1

Related Questions