Really delete defaulted special member functions

148 views
Skip to first unread message

frs.d...@gmail.com

unread,
Jun 16, 2013, 4:20:22 PM6/16/13
to std-dis...@isocpp.org, daniel....@gmail.com, Paolo Carlini
Hi

    During some work on libstdc++, even if the problem is not limited to this library, I plan to use the std::is_copy_assignable on std::pair and found out that it didn't work. std::is_copy_assignable<std::pair<const int, int>> for instance is not giving the std::false_type  answer. The reason of this wrong behavior is that the std::pair copy assignment operator is not defaulted. std::is_copy_assignable detects if the copy assignment exists or not, if it is not defaulted then it can't be deleted by the compiler and it will then always exist.

    Then, why isn't it defaulted ? Simply because we need it when std::pair contains a reference type. This following code is valid:

int i = 1;
std::pair<int&, int> p1(i, 0);
int j = 2;
std::pair<int&, int> p2(j, 0);
p1 = p2;
// i will now contains value 2.

    With a defaulted copy assignment the compiler would delete it because a reference cannot be assign once it has been initialized. The manually written version can give another meaning to the copy assignment which is not a pure copy anymore but which handle correctly the reference use case. So for the moment we must chose between a correctly behaving pair when instantiated with reference type or a working std::is_copy_assignment meta function.

    To solve this problem it is interesting to note that the std::pair has another assignment operator:

template<class U, class V> pair& operator=(const pair<U, V>&);

    This operator could be used to cover the reference use case, with U and V being respectively the T1 and T2 types of the pair. For the moment the compiler can't use it because overload resolution forces him to consider non-template version first which is fine. This is where a core language modification could help, I would like to propose that:

    Deleted special member functions shall not be considered during overload resolution.

    With such a modification, the copy assignment operator being defaulted, the compiler would delete it but would still be able to call the template version and the code would still compile. Moreover, with a modification of the template operator like this one;

template<class U1, class U2>
typename enable_if<__and_<is_assignable<T1&, const U1&>,
is_assignable<T2&, const U2&>>::value,
                                pair&>::type
     operator=(const pair<U1, U2>& p)

    The std::is_copy_assignable would start working properly as for std::pair<const int, int> the copy assignment operator would be deleted and the enable_if would make compilation of the template assignment to fail too. Interestingly, this way, the defaulted copy assignment comes back to pure copy semantic while the other assignment operator will handle all other use cases.

    Note that during resolution of DR 1402 a similar proposal has been approved but was limited to move semantic. Here I am proposing to extend it to any special member function, implicitly or explicitly defaulted.

    Does it sound like a reasonnable proposal ?

François Dumont

David Rodríguez Ibeas

unread,
Jun 19, 2013, 10:10:21 AM6/19/13
to std-dis...@isocpp.org
I understand that the proposal under discussion would be:

IMPLICITLY deleted special member functions shall not be considered during overload resolution.

Otherwise it would be confusing for users that explicitly delete the member function. Right?

P.S. Not sure whether 'implicitly' would be the correct term, but you get the idea



--
 
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussio...@isocpp.org.
To post to this group, send email to std-dis...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-discussion/.
 
 

François Dumont

unread,
Jun 20, 2013, 4:13:57 PM6/20/13
to std-dis...@isocpp.org
To be more accurate what I would like to see added to the Standard is:

Implicitly or explicitly defaulted special members that got deleted
shall not be considered during overload resolution.

I hadn't considered the explicitly deleted special members. In the
context of special members it might not be such a problem as when you
delete a copy constructor you will likely not introduce another
constructor that will be a match for the overload resolution.

I also forgot to mention that a discussion on this topic has
already taken place on the libstdc++ mailing list which result in this post:

http://gcc.gnu.org/ml/libstdc++/2013-04/msg00047.html

To sum up I have experimented this proposal on gcc/libstdc++ and
built all the testsuite with only one regression demonstrated by the
following code:

template <typename T>
class A : __not_copy_constructible_if<T>
{
A(const A&) = default;
};

class B : public A
{
B(const B&) = default;
B(const A&)
{
//...
}
};

Depending on some condition on T __not_copy_constructible will have a
deleted copy constructor and so will be A copy constructor and B one.
However, in this case, with the proposal, testing if B can be built from
const B& will succeed because it will consider the constructor from
const A&. I don't know yet how to handle this problem.

Fran�ois

Nevin Liber

unread,
Jun 20, 2013, 4:26:51 PM6/20/13
to std-dis...@isocpp.org
On 20 June 2013 15:13, François Dumont <frs.d...@gmail.com> wrote:

    I hadn't considered the explicitly deleted special members. In the context of special members it might not be such a problem as when you delete a copy constructor you will likely not introduce another constructor that will be a match for the overload resolution.

We run into this problem with frightening regularity now:

struct Foo
{
    template<class T>
    Foo(T&&) { /* better match for "copies" of non-const objects */ }
};

I'd rather not exasperate it.  In addition, the simplest workaround, adding a Foo(Foo&) constructor, can no longer be used, because it too is a special member function.
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com(847) 691-1404

François Dumont

unread,
Jun 22, 2013, 3:46:00 PM6/22/13
to std-dis...@isocpp.org
Could you give a more complete explanation of the problem that might arise from the proposal ?

Your Foo struct looks like the Gnu std::pair definition. The copy constructor is defaulted and the template constructor that could be used if the proposal was adopted are decorated in such a way that their definition won't be compilable. In your case it would be something like:

struct Foo
{
    template<class T,
                    class = typename std::enable_if</* some condition invalid for this constructor */>::type >
    Foo(T&&) { /* ... */ }
};

But in the case of the Gnu Standard library those std::enable_if invocation are already there to protect from some invalid instantiations. I wonder if those std::enable_if should not already be there in your case, no ?

François

Nevin Liber

unread,
Jun 24, 2013, 3:24:00 PM6/24/13
to std-dis...@isocpp.org
On 22 June 2013 14:46, François Dumont <frs.d...@gmail.com> wrote:
On 06/20/2013 10:26 PM, Nevin Liber wrote:
On 20 June 2013 15:13, François Dumont <frs.d...@gmail.com> wrote:

    I hadn't considered the explicitly deleted special members. In the context of special members it might not be such a problem as when you delete a copy constructor you will likely not introduce another constructor that will be a match for the overload resolution.

We run into this problem with frightening regularity now:

struct Foo
{
    template<class T>
    Foo(T&&) { /* better match for "copies" of non-const objects */ }
};

I'd rather not exasperate it.  In addition, the simplest workaround, adding a Foo(Foo&) constructor, can no longer be used, because it too is a special member function.

Could you give a more complete explanation of the problem that might arise from the proposal ?

Your Foo struct looks like the Gnu std::pair definition. The copy constructor is defaulted and the template constructor that could be used if the proposal was adopted are decorated in such a way that their definition won't be compilable. In your case it would be something like:

struct Foo
{
    template<class T,
                    class = typename std::enable_if</* some condition invalid for this constructor */>::type >
    Foo(T&&) { /* ... */ }
};

Sure, I can write that.  But a non-expert?

The fact that we need *anything* to differentiate between:

Foo f;
const Foo cf;
Foo copycf(cf); // calls copy constructor
Foo copyf(f) // calls templated constructor

is a language problem that trips many developers up.  Forcing developers to use SFINAE to get around this problem is an aberration.

The current workaround I recommend is:

Foo(Foo& that) : Foo(static_cast<Foo const&>(that)) {}


It seems that making the rules even more convoluted just to satisfy the world's most complicated struct of two elements feels like the wrong approach to me.  Will such a change silently break users code where they have explicitly deleted functions so that they no longer compile?

If we want a separate category of deleted functions that aren't part of the overload set, let's mark them differently than "=delete".

Does Concepts Lite solve this?

Ville Voutilainen

unread,
Jun 24, 2013, 3:26:43 PM6/24/13
to std-dis...@isocpp.org
On 24 June 2013 22:24, Nevin Liber <ne...@eviloverlord.com> wrote:

If we want a separate category of deleted functions that aren't part of the overload set, let's mark them differently than "=delete".


=suppress often comes to mind.

François Dumont

unread,
Jun 26, 2013, 4:46:40 PM6/26/13
to std-dis...@isocpp.org
On 06/24/2013 09:24 PM, Nevin Liber wrote:
On 22 June 2013 14:46, François Dumont <frs.d...@gmail.com> wrote:
On 06/20/2013 10:26 PM, Nevin Liber wrote:
On 20 June 2013 15:13, François Dumont <frs.d...@gmail.com> wrote:

    I hadn't considered the explicitly deleted special members. In the context of special members it might not be such a problem as when you delete a copy constructor you will likely not introduce another constructor that will be a match for the overload resolution.

We run into this problem with frightening regularity now:

struct Foo
{
    template<class T>
    Foo(T&&) { /* better match for "copies" of non-const objects */ }
};

I'd rather not exasperate it.  In addition, the simplest workaround, adding a Foo(Foo&) constructor, can no longer be used, because it too is a special member function.

struct Foo
{
    template<class T,
                    class = typename std::enable_if</* some condition invalid for this constructor */>::type >
    Foo(T&&) { /* ... */ }
};

Sure, I can write that.  But a non-expert?

Indeed but I miss the fact that enable_if is here only to make the type meta programing friendly. In the case of the std::pair it is not the enable_if that generate compilation failures it is the code itself. For instance:

std::pair<const int, int> p1, p2;
p1 = p2;

won't compile, with or without the proposal and with or without enable_if. enable_if is just here to make std::pair work with std::is_copy_assignable by hiding the return type of the expression when it is not supposed to compile. Non-expert simply won't deal with it.



The fact that we need *anything* to differentiate between:

Foo f;
const Foo cf;
Foo copycf(cf); // calls copy constructor
Foo copyf(f) // calls templated constructor

is a language problem that trips many developers up.  Forcing developers to use SFINAE to get around this problem is an aberration.

Do you know this link:
http://thbecker.net/articles/rvalue_references/section_08.html

It is a very good explanation about what is going on here.

In my opinion, introducing such a template constructor already requires good expertise, not really less than what you need to start using enable_if. And once again you will surely not need enable_if to make the compilation of the expression fail.


The current workaround I recommend is:

Foo(Foo& that) : Foo(static_cast<Foo const&>(that)) {}


It seems that making the rules even more convoluted just to satisfy the world's most complicated struct of two elements feels like the wrong approach to me.
In my opinion this proposal is rather a Standard enhancement which adds consistency for 2 reasons:

First, even if English is not my mother tongue, the keyword 'delete' has a clear meaning which is that something deleted should not exist anymore. With the current Standard something deleted can have a side effect on the overload resolution, it really surprises me when I realized it.

Second, there is already the DR 1402 resolution which says more or less that:

Defaulted move constructor or assignment operators shall not be considered by overload resolution if deleted.

If this happens to be a solution to the DR 1402 issue and it can also be a solution to the issue exposed here then why not generalize it, another consistency enhancement.


  Will such a change silently break users code where they have explicitly deleted functions so that they no longer compile?

Silently, surely not, it will show up at compilation time. This is also the point of this discussion, can you see a code sample that won't compile anymore but should ? I already presented one detected when testing this proposal on gcc/libstdc++ which is in fact a non-issue as it rather comes from a bad design. Now I would like to know if anyone can find another code sample that won't compile with this proposal but should.


If we want a separate category of deleted functions that aren't part of the overload set, let's mark them differently than "=delete".

Does Concepts Lite solve this?

I don't fully get you here. If we can do without another language keyword this is better if this is the question.

Nevin Liber

unread,
Jun 26, 2013, 5:14:15 PM6/26/13
to std-dis...@isocpp.org
On 26 June 2013 15:46, François Dumont <frs.d...@gmail.com> wrote:

First, even if English is not my mother tongue, the keyword 'delete' has a clear meaning which is that something deleted should not exist anymore. With the current Standard something deleted can have a side effect on the overload resolution, it really surprises me when I realized it.

And by overloading the meaning of a "deleted" function (suppressed in the case of copy/move constructors but still part of the overload set for other functions) pretty much obfuscates any clarity.

Every time we overload a keyword or syntax for multiple purposes we make the language significantly harder to understand and use.  Is '&&' when applied to a type an r-value reference?  It depends.  Is "this" a pointer to the current instantiated object?  It depends.  Can I add parentheses to an expression without changing its meaning?  It depends.  Etc., etc.

Not adding new keywords or syntaxes for orthogonal concepts protects current code bases at the cost of all future code bases.

'delete' should either always suppress a function from the overload set, or 'delete' should never suppress a function from the overload set.  No more "it depends", please.

We could always use "= delete default" to mean suppress it from the overload set (which is at least a different syntax, even though we are overloading keywords).
Reply all
Reply to author
Forward
0 new messages