Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Friending classes defined in the std namespace: any guarantees?

This question came up as I answered this question: does the standard allow and make any guarantees about friend-ing standard library classes and/or functions?

In this particular case, the situation the question was whether:

class MyUserDefinedType
{
    friend struct std::default_delete<MyUserDefinedType>;

private:
    ~MyUserDefinedType() { }
}

is guaranteed to allow MyUserDefinedType to be stored in a std::unique_ptr<MyUserDefinedType> or std::shared_ptr<MyUserDefinedType> object with the default deleter.

In general, are classes described in the standard library required to implement their functionality directly, or can they use any arbitrary level of indirection? For example, is it possible that

  • std::default_delete<MyUserDefinedType> is actually a using alias of a class defined in an inner namespace of std, in which case the friend declaration would be illegal

or

  • std::default_delete<MyUserDefinedType> calls some other class that actually does the deleting, in which case the friend declaration would not have the desired effect

or something else along those lines?

My guess is that this is UB not guaranteed to work but I am curious if this is addressed specifically by the standard.

This specific example given above works for clang trunk (w/libc++) and GCC 4.7.2 (w/libstdc++), FWIW

like image 497
Stephen Lin Avatar asked Mar 04 '13 06:03

Stephen Lin


2 Answers

is it possible that std::default_delete<MyUserDefinedType> is actually a using alias of a class defined in an inner namespace of std, in which case the friend declaration would be illegal?

No. Per Paragraph 20.7.1.1.2 of the C++11 Standard:

namespace std {
    template <class T> struct default_delete {
        constexpr default_delete() noexcept = default;
        template <class U> default_delete(const default_delete<U>&) noexcept;
        void operator()(T*) const;
    };
}

The fact that it has to be a class template is explicitly specified. This means it cannot be an alias template. If that was the case, it would also be impossible to specialize it.

is it possible that std::default_delete<MyUserDefinedType> calls some other class that actually does the deleting, in which case the friend declaration would not have the desired effect?

Yes. Nothing in the Standard specifies that the call cannot be done by some internal helper. Per Paragraph 20.1.1.2:

void operator()(T *ptr) const;

3 Effects: calls delete on ptr.

4 Remarks: If T is an incomplete type, the program is ill-formed.

This only specifies what the effect of invoking the call operator on the default_delete<> functor should be, not how this shall be achieved concretely (whether directly inside the body of the call operator, or by delegating the task to some member function of some other class).

like image 94
Andy Prowl Avatar answered Oct 02 '22 10:10

Andy Prowl


In general, are classes described in the standard library required to implement their functionality directly, or can they use any arbitrary level of indirection?

In general an implementation may indirect as much as it wants. Have e.g. a look at the implementations of standard containers and their iterators - or just use them wrong and see which templates are involved from the error messages. However, since default_delete is nothing magic, that should be a one-liner, you can expect it to do the work itself, but it's not guaranteed.

My guess is that this is UB but I am curious if this is addressed specifically by the standard.

It's not UB, it's just unspecified.

You could be sure if you just specialized default_delete<MyUserDefinedType> (it is allowed to specialize standard libraray templates), but I would not do that.

I would not use friendship at all, especially not if it comes to templates that have not been specialized. Consider this:

//your code
class MyUserDefinedType
{
    friend struct std::default_delete<MyUserDefinedType>; //for deletion
private:  
    int veryPrivateData;
    ~MyUserDefinedType() { }
};

//evil colleague's code:
namespace std {
  //we may specialize std-templates for UDTs...
  template<>
  struct default_delete<MyUserDefinedType>
  {
    constexpr default_delete() noexcept = default;
    template <class U> default_delete(const default_delete<U>&) noexcept {}
    void operator()(T* pt) const { delete pt; }

    //sneaky...
    void access(MyUserDefinedType& mudt, int i) const
    { mudt.veryPrivateData = i; }
  };
}

void somewhere_deep_in_the_code()
{
  MyUserDefinedType& myUDT = /*something...*/;
  std::default_delete<MyUserDefinedType>().access(myUDT, 42); //tricked you!
}

Friends can do anything to you. Choose them with caution. In this case, I'd really recommend a custom deleter - assuming it makes any sense making the destructor private but providing access to it via the deleter.

like image 36
Arne Mertz Avatar answered Oct 03 '22 10:10

Arne Mertz