最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - Why are deleted member functions not propagated through derived classes? - Stack Overflow

programmeradmin3浏览0评论

Given the following C++ code snippet:

#include <type_traits>
#include <iostream>

template <typename T>
struct Bar
{
    Bar() = default;
    Bar(const Bar&) = delete;
};

template <typename T>
struct Foo : public Bar<T>
{
    Foo() = default;
    Foo(const Foo& other) : Bar<T>(other) {}
};

int main() {

    std::cout << std::is_copy_constructible_v<Bar<int>> << '\n'; // prints 0
    std::cout << std::is_copy_constructible_v<Foo<int>> << '\n'; // prints 1

    return 0;
}

, does anybody know why Foo is reported to be copy-constructible when it clearly isn't? In other words, why is deleted copy constructor of Bar not propagated through Foo?

Regarding std::is_constructible, the standard does say: "Only the validity of the immediate context of the variable definition is considered", but I do not see why would they decide to define it like that.

Finally, does this mean that, without an explicit check within Foo, it is absolutely impossible to discover that Bar is not copy-constructible by solely inspecting Foo, with or without modifying Bar?

UPDATE:

As it seems my initial question was not clear enough, let me update the post.

I am not asking what is the rule that governs the reported behavior, but rather why are the rules defined as such, since it seems to be unintuitive.

Furthermore, nobody seems to answer my central question: if some function foo does not know how is Foo implemented and does not even know that Bar exists, is it impossible for it to discover that Foo cannot be copied, or is it inevitably doomed to fail to compile?

Given the following C++ code snippet:

#include <type_traits>
#include <iostream>

template <typename T>
struct Bar
{
    Bar() = default;
    Bar(const Bar&) = delete;
};

template <typename T>
struct Foo : public Bar<T>
{
    Foo() = default;
    Foo(const Foo& other) : Bar<T>(other) {}
};

int main() {

    std::cout << std::is_copy_constructible_v<Bar<int>> << '\n'; // prints 0
    std::cout << std::is_copy_constructible_v<Foo<int>> << '\n'; // prints 1

    return 0;
}

, does anybody know why Foo is reported to be copy-constructible when it clearly isn't? In other words, why is deleted copy constructor of Bar not propagated through Foo?

Regarding std::is_constructible, the standard does say: "Only the validity of the immediate context of the variable definition is considered", but I do not see why would they decide to define it like that.

Finally, does this mean that, without an explicit check within Foo, it is absolutely impossible to discover that Bar is not copy-constructible by solely inspecting Foo, with or without modifying Bar?

UPDATE:

As it seems my initial question was not clear enough, let me update the post.

I am not asking what is the rule that governs the reported behavior, but rather why are the rules defined as such, since it seems to be unintuitive.

Furthermore, nobody seems to answer my central question: if some function foo does not know how is Foo implemented and does not even know that Bar exists, is it impossible for it to discover that Foo cannot be copied, or is it inevitably doomed to fail to compile?

Share Improve this question edited Feb 16 at 11:48 lobelk asked Feb 15 at 16:19 lobelklobelk 4962 silver badges11 bronze badges 12
  • 2 This is because the copy constructor is not SFINAE-friendly. – Patrick Roberts Commented Feb 15 at 16:26
  • 2 It's your Foo(const Foo& other) : Bar<T>(other) {} that makes is_copy_constructible_v evaluate to true. – Ted Lyngmo Commented Feb 15 at 16:27
  • 4 "why Foo is reported to be copy-constructible when it clearly isn't?" -- You are misusing "clearly" because from my perspective, it "clearly" is copy-constructible (because you declared that it has a copy constructor). – JaMiT Commented Feb 15 at 16:38
  • 1 Yes, the Bar's copy constructor is not SFINAE-friendly. It's the problem of Bar, not of whatever code is interacting with it. I don't really understand the question, since you already know about the immediate context. – HolyBlackCat Commented Feb 15 at 17:26
  • 1 @lobelk Foo is clearly declared by the programmer to be copy constructible, however. See my updated answer for my details. – Alexandre S. Commented Feb 16 at 14:50
 |  Show 7 more comments

4 Answers 4

Reset to default 3

In short, if you write a function body, then the function isn't deleted. A function is deleted if you write = delete; in lieu of the function body. (Technically, the standard considers = delete; to be a function body, but that's pedantry.) Also, if you write = default;, the function can end up deleted when certain circumstances arise that cause failure to generate the implementation. But if you write the body yourself, then the compiler assumes you meant for there to be a body, not for the function to be deleted!

In your case,

Foo(const Foo& other) = default;

will cause that constructor to be deleted, since it would need to use a deleted constructor of the base class.

Also, as a general rule, if you write a function body and only the body is ill-formed, then the function will appear to be callable when you query whether it's callable, either through a library-provided trait, or a requires-expression, or any other SFINAE context. This is because function bodies are not in the immediate context of the expression that contains the function call. You can only detect well-formedness in the immediate context.

Deleted special functions actually are propagated to child classes.

However, you have declared your own copy constructor in Foo, so you are saying to the compiler "Here is how to copy a Foo, even though you may not know how to copy a Bar"

Of course, if you actually instanciate this function by calling it, the call to Bar's deleted copy constructor will be a compilation error. Note that if Foo wasn't a template, the code would not compile at all.

If all your copy constructor does is forward to the parent's copy constructor (and it should, look up rule of five/rule of zero), you should either not declare those special member functions at all and let tgem be generated by the compiler, or declare them as = default;. In both cases, the compiler will ensure the function is deleted if some parents' (or members') are.

Update

The rules simply say that a class with a copy constructor is copy constructible, independently of how you choose to implement it, which is quite sensible. It is up to Foo to provide a valid implementation, and the rest of the code should be free to assume that any accessible function is actually callable.

The fact that your implementation is not valid in some cases is a different concern. The problem is that Foo is a template, so its member functions are instantiated only when used. This is why std::is_copy_constructible manages to see that your class does have a copy constructor, but as it doesn't try to use it no instantiation happens and the compiler doesn't catch the error.

Note that I said "in some cases" because there could be situations where it is valid, for instance if someone adds a specialization of Bar that is copy constructible for some T. This is why the compiler cannot catch the error from the template alone, and waits until instantiation.

So in short, Foo (as it is written) is a copy constructible class, which happens to have a faulty implementation. But from external code you can only ask "Does Foo have a copy constructor?" (yes), and not "Does Foo have a copy constructor with no compilation error?" (no). Just because the error happens only when you try to use it from external code does not mean that this code is at fault: it is an error in Foo, just detected late by the compiler.

And to fix this error, you must either change the constructor's implementation to work in all circumstances, or use =default;, =delete; or clever SFINAE to make the copy constructor inaccessible when you can't implement it.

I am not asking what is the rule that governs the reported behavior, but rather why are the rules defined as such, since it seems to be unintuitive.

The answer to this is that C++ is built around the idea of separate compilation. Separate translation units can be built independently. We thus also have a distinction between declarations and definitions, and you must have the declaration to use an entity, but often you do not need the definition.

So how do you decide if an operation is valid? Do you need the definition of the entity you are using and then anything it uses and so on... all the way down? Or do you focus on the declaration, what the author has told us is valid?

C++ very consistently chooses to look at the declaration for SFINAE, type traits, and even concepts. Yes, that means the author could have messed up and said a type is copyable when it isn't, but it also means we don't need the body of that function (and then everything it needs to be complete) to decide if it is copyable and that function can live in a source file separately compiled.

Foo is reported as copy-constructible because it explicitly defines a copy constructor, and std::is_copy_constructible only checks whether such a constructor exists and is not explicitly deleted—it does not check if calling it would result in an error. The compiler allows Foo to define a copy constructor even though it attempts to copy Bar, whose copy constructor is deleted. However, actually invoking Foo's copy constructor would result in a compilation error.

So the main point is how to Fix This? If you want Foo to be truly non-copyable, explicitly delete its copy constructor:

template <typename T>
struct Foo : public Bar<T>
{
    Foo() = default;
    Foo(const Foo&) = delete;  // Explicitly delete copy constructor
};

Now, std::is_copy_constructible_v<Foo> will correctly print 0, meaning Foo is not copy-constructible.

发布评论

评论列表(0)

  1. 暂无评论