Private constructor inhibits use of emplace[_back]

2019-02-12 07:33发布

问题:

Consider the following code:

#include <vector>

class A
{
public:    
    A(A&&);  // somewhat expensive

    static std::vector<A> make_As()
    {
        std::vector<A> result;
        result.push_back(A(3));
        result.push_back(A(4));
        return result;
    }

private:
    A(int);  // private constructor
};

Since A's move constructor is somewhat expensive (for whatever reason), I'd like to avoid calling it and use emplace_back() instead:

#include <vector>

class A
{
public:    
    A(A&&);  // somewhat expensive

    static std::vector<A> make_As()
    {
        std::vector<A> result;
        result.emplace_back(3);
        result.emplace_back(4);
        return result;
    }

private:
    A(int);  // private constructor
};

Unfortunately, with emplace_back(), the actual constructor call is done by something in the standard library, which is not privileged enough to be able to call A's private constructor.

I realize that there's probably little that can be done about this, but nonetheless I feel that since the calls to emplace_back() occur within a member of A, they ought to be able to call the private constructor.

Are there any workarounds for this?

The only thing I can think of is to add a friend-declaration to A, but the precise class that needs to be A's friend (that is, the class that actually tries to invoke the constructor) is implementation-specific (for example, for GCC it's __gnu_cxx::new_allocator<A>). EDIT: just realized that such a friend declaration will allow anyone to emplace_back() A's constructed with the private constructor into a container of A's, so it wouldn't really solve anything, I might as well make the constructor public at that point...

UPDATE: I should add that A's move constructor being expensive is not the only reason to avoid having to call it. It may be that A is not movable at all (nor copyable). That would not work with vector, of course, (because emplace_back() may have to reallocate the vector), but it would with deque, which also has a similar emplace_back() method but does not have to reallocate anything.

回答1:

One possible workaround (or kludge) would be to use a helper class to hold the parameters to the private ctor of A (let's call this class EmplaceHelper). EmplaceHelper should also have a private ctor, and it should be in mutual friendship with A. Now all you need is a public ctor in A that takes this EmplaceHelper (via const-ref, probably), and use that with emplace_back(EmplaceHelper(...)).

Since EmplaceHelper can only be constructed by A, your public ctor is still effectively private.

It might even be possible to generalize this idea with a templated EmplaceHelper (possibly using std::tuple to hold the ctor parameters).

Edit: actually, it seems I overcomplicated this as a comment below from GManNickG gave me a simpler idea: EmplaceHelper could be just an empty class (still with private ctor, in friendship with A as above) but it needn't store all ctor params, just modify your private ctor to include EmplaceHelper as the first (or last) parameter (and make it public). The effect would be the same as above, but without the complicated logic in EmplaceHelper.

Like this:

class A 
{ 
private:
    struct private_key {};

public:     
    A(int x, const private_key&) :
    A(x) // C++11 only, delegating constructor
    {}

    A(A&&);  // somewhat expensive 

    static std::vector<A> make_As() 
    { 
        std::vector<A> result; 
        result.emplace_back(3, private_key()); 
        result.emplace_back(4, private_key()); 
        return result; 
    } 

private: 
    A(int);  // private constructor 
}; 

If delegated constructors are not available, you can either factor out the common code for each version, or just get rid of A(int) altogether and only use the new version, possibly with a defaulted second parameter.



回答2:

By the C++11 standard, all of the standard containers should use the allocator::construct method to do in-place construction. As such, you could simply make std::allocator a friend of A.

I suppose technically this function is allowed to delegate the actual construction call to something else. Personally, I think the spec should be a little more strict about enforcing exactly which objects call constructors and what can and cannot be delegated.

If such delegation occurs, or for whatever reason, you could provide your own allocator that forwards all calls to std::allocator except for construct. I don't suggest the latter, as many standard container implementations have special code for dealing with std::allocator that allows them to take up less space.

just realized that such a friend declaration will allow anyone to emplace_back() A's constructed with the private constructor into a container of A's, so it wouldn't really solve anything, I might as well make the constructor public at that point...

Then you're going to have to decide what's more important to you: in-place construction, or hiding privates. By it's very nature, in-place construction means that someone not in your code is doing the construction. Therefore, there's no way around it: some external code must be named a friend or the constructor must be public. In short, the constructor must be publicly accessible to whomever is delegated the construction.



回答3:

Let's simplify a bit. The following fails to compile, because V has no access to A's private constructor.

struct V
{
    E(int i)
    {
        // ...
        auto a = A(i);
        // ...
    }
};

Going back to your code, V is just a simplification of vector, and V::E is just a simplification of what emplace_back does. vector doesn't have access to A's private constructor, and vector::emplace_back needs to call it.