braced-initialization allows creation of temporary of a *private* struct
Asked Answered
L

1

6

I just read the following article from Raymond Chen's excellent 'The Old New Thing': https://devblogs.microsoft.com/oldnewthing/20210719-00/?p=105454

I have a question about this, best described in the following code snippet. Why is the initialization of 'x3' allowed at all? I don't see any semantic difference between the initialization of 'x2' and 'x3' below.

#include <memory>

class X
{
    struct PrivateTag {};   // default constructor is *NOT* explicit
public:
    X(PrivateTag) {}
    static auto Create() -> std::unique_ptr<X> 
    {
        return std::make_unique<X>(PrivateTag{});
    }
};

int main()
{
    auto x1 = X::Create();          // ok:    proper way to create this
  //auto x2 = X(X::PrivateTag{});   // error: cannot access private struct
    auto x3 = X({});                // ok:    why ?!
}
Leal answered 24/7, 2022 at 0:26 Comment(0)
V
8

Accessibility restricts only which names may be written out explicitly. It does in no way prevent usage of a type.

It is always possible to e.g. use decltype on a function returning the private type and then give it a new name and access it fully through that name or to use template argument deduction to give inaccessible types new aliases through which they can be accessed.

The best you can do is to make it hard for a user to accidentally misuse the constructor by employing methods as discussed in the linked blog post, but if a user is determined enough, they can pretty much always obtain a fully accessible alias for PrivateTag and use the constructor anyway.

auto x3 = X({}); does not spell out the member classes name, so accessibility of it doesn't matter.

auto x2 = X(X::PrivateTag{}); does spell the name of the member class, so accessibility needs to be considered, which makes it ill-formed since PrivateTag is private and inaccessible in the context where it is named.


Here is an example of circumventing the private tag trick, no matter whether the adjustment suggested in the blog post is made:

struct any_convert {
    template<typename T>
    operator T() const {
        // Here `T` becomes an alias for `PrivateTag`
        // but is completely accessible.
        return T{};
    }
};

//...

auto x4 = X(any_convert{});
Volvulus answered 24/7, 2022 at 0:31 Comment(3)
Then we give PrivateTag a private constructor and befriend the outer class?Pentothal
@PasserBy You need to add some member and make it non-implicit-lifetime, otherwise I can just cast a suitably aligned pointer to the type without calling a constructor. Aside from that I am not sure whether there is another loophole, but I wouldn't be surprised if there is.Volvulus
Thanks @Volvulus - very nicely written (and helpful) answer!Leal

© 2022 - 2024 — McMap. All rights reserved.