0

I want to learn more about C++ and just came along a - in my eyes - very weird behavior.

Question Summary

Since the detailed explanation is a bit long, here is a summary:

I've a Container with two member variables, of which only one can be filled with a public constructor. Both can be filled only in private. I've overloaded the copy and move for operator= in a way that the second data member can't be filled/accessed with the. BUT when returning a container with both member variables filled and writing something like

Container containerB = Container::returnContainerWithBothMembersFilled();

containerB has both member variables filled. This seems weird to me and that's not what I need/want.

Can someone explain to me how to solve that and why this is happening?

Detailed Explanation

In order to explain the behavior in more detail, here is a more detailed setup that reproduces my behavior:

Let's assume a class Container with two private members dataA_ and dataB_ of type std::vector<int>. There exists a public constructor to initialize dataA_ (but not! dataB_) and a private constructor to initialize both private members.

class Container {
public:
  Container() = default;
  Container(const Container& vector) = delete;

  explicit Container(std::vector<int> data) : dataA_(std::move(data)) {}

  // ... 

private:
  std::vector<int> dataA_;
  std::vector<int> dataB_;

  explicit Container(std::vector<int> dataA, std::vector<int> dataB)
      : dataA_(std::move(dataA)), dataB_(std::move(dataB)) {}
};

Further, there is a function static Container func() that returns an instance of Container with both data members set to some value.

  static Container func() {
    return Container(std::vector<int>(3,1), std::vector<int>(3,2));
  }

I have overloaded operator= in 2 different variants:

  • Container& operator=(const Container& other)
  • Container& operator=(Container&& other)

where all 2 variants have the same definition shown here for the first overload exemplary:

  Container& operator=(const Container& other) {
    if (this != &other) {
      dataA_ = other.dataA_;
    }
    return *this;
  }

FYI: all code above is written within class Container { ... };!

Question

Why does the following give me a container2 with a 'filled' and not empty dataB_?

int main() {
  std::cout << "init:" << std::endl;
  Container container1(std::vector<int>({1, 2, 3}));
  Container container2 = container1.func(); // doesnt work as expected
  Container container3;
  container3 = container1; // works, overloaded operator= is called and behavior is as expected

  return 0;
}
fakl
  • 127
  • 7
  • Yeah I just realized that and edited the post accordingly. I found how i get a behavior as expected as shown above in the last code example. but ```container2``` still isn't as expected. @user17732522 – fakl Aug 23 '22 at 23:45
  • Because `container1.func()` returns a `Container` with both class members initialized, because that's the constructor that it calls to construct the returned object? That's literally what the code does, verbatim. What is so unclear about that? There's nothing mysterious here. This is The Golden Rule Of Computer Programming, in action: "your computer always does exactly what you tell it to do instead of what you want it to do". – Sam Varshavchik Aug 23 '22 at 23:46
  • @SamVarshavchik yeah I am aware of that, but shouldn't ```operator=``` be called in the line with ```container2```? And following this thought the ```dataB_``` should be ignored. Obviously this isn't what's happening but why and how would I need to change the code in order to ignore ```dataB_``` for the line with ```container2```? – fakl Aug 23 '22 at 23:49
  • `operator=` is called to assign something to an object that's already constructed. `operator=` is not, itself, a constructor. Do you expect `container2` to already be constructed somehow, somewhere already? There are many other punctuations that are used in different ways, in C++. `&` can mean three different things, depending on where it is used, and how. Here, this `=` does not mean "assignment". – Sam Varshavchik Aug 23 '22 at 23:51
  • Even if you don't want to move stuff into `dataB_`, please, at least `{}` initialise it. – bitmask Aug 23 '22 at 23:52
  • OK, I see that seems logic to me! I didn't initialize ```dataB_``` here in order to reduce it to the necessary code, but thanks! – fakl Aug 23 '22 at 23:53
  • 1
    Related reference: [What's the difference between assignment operator and copy constructor?](https://stackoverflow.com/questions/11706040/) *(Maybe not needed by the OP, but could be helpful to future visitors.)* – JaMiT Aug 24 '22 at 06:02

1 Answers1

2
Container container2 = container1.func();

This is copy-initialization, not assignment, so operator= is irrelevant.

Before C++17 the initialization would be ill-formed, because it requires a constructor to be available which accepts a Container rvalue as argument (the result of the call container1.func()). But you deleted the copy constructor and as a consequence there is also no move constructor either.

Since C++17 the initialization uses mandatory copy elision, meaning that the return value of container1.func() is directly constructed into container2 (because container1.func() is a prvalue of the same class type as container2). There is no constructor call involved either. You just get the return value from container1.func() in container2 and that one is constructed with both members filled.

Before C++17, even if the copy constructor was not deleted, but defined equiavalently to your operator=, it would still not work the way you want it to because the compiler was still allowed, but not required to elide the move/copy constructor call as is now mandatory since C++17. It would be up to the compiler whether or not container2 would have the second member empty. That's one of the reasons why it is a bad idea to change the semantics of the copy/move operations. This is one of a small number of situations where the compiler is allowed to perform optimizations which change the observable behavior of the program.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Thanks, OK, that makes sense to me! Is there a possibility to still ignore the values of ```dataB_``` in the line with ```container2```? – fakl Aug 23 '22 at 23:51
  • 1
    @fakl Not really. You have a function returning a `Container` with both members filled. If that is not how the return value of the function is supposed to be used, why does it return such a container? – user17732522 Aug 23 '22 at 23:53
  • @fakl `friend Container onlyA(Container&& c) { c.dataB_.clear(); return c; }` – bitmask Aug 23 '22 at 23:57
  • @user17732522 The overall setup is way more complex than this example. I just constructed an that shows the same behavior. In the actual implementation ```dataB_``` contains references to another ```Container```'s ```dataA_``` in order to be able to modify them with some operators/functions. All this is because I am actually working on a mathematical vector implementation that allows me similar stuff like in MATLAB, i.e. accessing/modifying partial vectors. – fakl Aug 23 '22 at 23:57
  • @bitmask I came along a similar idea, basically, either clearing ```dataB_``` as user of this code everytime I copy it like in the above for ```container2``` or deleting it in every member function/operator of ```Container```, but that seemed a bit bad, since the actual '```Container```' has around 40 member functions. – fakl Aug 24 '22 at 00:01
  • @fakl I don't think that really affects my question. `func` returns a `Container` by value. The caller should be able to retrieve and use this `Container` object. If the caller should not retrieve an object with the second member filled, then `func` should clear it before returning it. – user17732522 Aug 24 '22 at 00:01
  • Also, if the class stores references to other `Container` objects, then the `Container` class should probably not be copyable or movable at all. If it is, copies and moves will invalidate these references. You can only make it work properly if the objects have back-references as well so that they can update references kept to them by other objects when they are moved or copied. – user17732522 Aug 24 '22 at 00:02
  • Basically what I want is something like this: modifying a container with ```operator=``` i.e. by setting all it's ```dataA_``` vector values to a single value, i.e. ```container1.func() = 0;``` (in the original code there exists a ```operator=(int value)``` for that purpose) and copying just ```dataA_``` even this ```func()``` call returns both i.e. like this: ```Container container2 = container1.func()```; – fakl Aug 24 '22 at 00:08
  • So is there an option to delete ```dataB_``` at the line of ```container2```? Or to generalize that: is there an option to recognize in ```func()``` if it is on the left or right side of ```=```? That sounds quite stupid tbh but I don't know how to explain what I want in an other way – fakl Aug 24 '22 at 00:11
  • @fakl So what would like to happen if there is a function `void g(Container c);` and you call it with `g(container1.func())`? Should `c` now have both members filled? There is language-wise no difference between the initialization of this `c` parameter and of `container2`. The `=` sign just happens to be part of the copy-initialization syntax but has no special meaning and is completely unrelated to the assignment `=`. – user17732522 Aug 24 '22 at 00:15
  • In MATLAB you can write ```vec = [1 2 3 4]``` and then later do ```partVec = vec(1:2)``` or ```vec(1:2) = 0```. I am trying to implement that in C++. Here MATLAB's ```()``` operator is represented with ```func()```. In order to change and access ```vec```'s elements I want a reference to the elements in the case of ```vec(1:2) = 0``` and a copy by value in the case of ```partVec = vec(1:2)```. In the real code the ```func()``` is supposed to do exactly that behavior of MATLAB's ```()``` operator – fakl Aug 24 '22 at 00:15
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247495/discussion-between-fakl-and-user17732522). – fakl Aug 24 '22 at 00:16
  • 2
    @fakl As far as I know MATLAB doesn't differentiate between assignment and initialization. In C++ these are two completely different constructs and you can't pretend that `Type var = init;` is the same as declaring `var` and then assigning to it. That both use a `=` character is coincidental in the syntax. `Type var = init;` is not related to `var = value` as an assignment expression. – user17732522 Aug 24 '22 at 00:18