1

Original post edited to provide more minimal reproduction.

Long-time C programmer here, relearning C++ for first time since about 2010, so I'm quite new to all the modern C++11 and beyond stuff. I think I understand the intent behind copy and move constructors and why one would explicitly define/default/delete them. However, I'm pretty lost on the interactions between the different constructor types in my own practice program:

TerritoryUSA.hpp

#ifndef TERRITORYUSA_HPP
#define TERRITORYUSA_HPP

namespace TerritoryUSA {
    enum class Identifier {
        Alabama,
        Alaska,
        American_Samoa
    };

    class Territory {
    private:
        Identifier identifier;
    public:
        // Construct.
        Territory(Identifier identifier);
        //Territory(Territory& other) = delete;         // 1
        //Territory(Territory&& other) = delete;        // 2

        // Move.
        //Territory& operator=(Territory& other) = delete;  // 3
        //Territory& operator=(Territory&& other) = delete; // 4

        // Destruct.
        ~Territory() = default;

        // Methods.
        Identifier getIdentifier();
    };
}

#endif /* TERRITORYUSA_HPP */

TerritoryUSA.cpp

#include "TerritoryUSA.hpp"

using namespace TerritoryUSA;

// Construct.
Territory::Territory(Identifier identifier) : identifier{identifier} {}

// Methods.
Identifier Territory::getIdentifier() {
    return identifier;
}

main.cpp

#include <cstdio>
#include "TerritoryUSA.hpp"

using namespace TerritoryUSA;

int main() {
    Territory territories[]{
        Territory(Identifier::Alabama),
        Territory(Identifier::Alaska),
        Territory(Identifier::American_Samoa)
    };

    for (Territory territory : territories) {
        printf("%d\n", static_cast<int>(territory.getIdentifier()));
    }
}

output

0
1
2
Program ended with exit code: 0

Note that this is done in Xcode on MacOS, which I'm also in the throes of figuring out as I mostly work with vim/make on Linux when writing C. I have't figured out where to find the compiler version yet, but this is a brand new updated Macbook with freshly installed Xcode, so it can't be very old. All I did was start a new Xcode "command line tool" application and put the 3 files in the project directory using the right click menu, so the build system should be correct (and it does build and run successfully when the marked lines are commented).

The part that's confusing the heck out of me is when I selectively uncomment the lines marked 1-4 in the header file, I get varying errors related to the construction of Territory objects. Here are the results I get when uncommenting single lines:

Line 1:

Lines 8-10 of main.cpp (where I try to instantiate the Territory objects) I get this red error:

No matching constructor for initialization of 'TerritoryUSA::Territory'

It also highlights in gray line 17 of the hpp (the constructor that takes an identifier argument) with this message:

1. Candidate constructor not viable: no known conversion from 'TerritoryUSA::Territory' to 'TerritoryUSA::Identifier' for 1st argument

It also highlights the uncommented Line 1 in gray with this message:

2. Candidate constructor not viable: expects an l-value for 1st argument

Line 2:

Lines 8-10 of main.cpp (where I try to instantiate the Territory objects) I get this red error:

Call to deleted constructor of 'TerritoryUSA::Territory'

It also highlights the uncommented Line 2 in gray with this message:

1. 'Territory' has been explicitly marked deleted here

Line 3:

No errors

Line 4:

Lines 8-10 of main.cpp (where I try to instantiate the Territory objects) I get this red error:

Call to implicitly-deleted copy constructor of 'TerritoryUSA::Territory'

It also highlights the uncommented Line 4 in gray with this message:

1. Copy constructor is implicitly deleted because 'Territory' has a user-declared move assignment operator

Questions

For Line 1, why is it trying to use a no-argument constructor and/or make any implicit assumptions? Haven't I provided a perfectly valid explicit constructor for the signature with 1 Identifier argument? I intentionally do not want to allow users to instantiate Territory objects without any arguments (quite frankly, I don't want them to instantiate them at all, but one step at a time). And also, what does deleting a copy constructor have to do with this? I'm not trying to copy anything.

For Line 2, again, I understand that I deleted the default constructor, but I'm not trying to use it, I'm trying to use the one that takes an identifier. Why am I getting tripped up? And also, what does deleting a move constructor have to do with this? I'm not trying to move anything.

For Line 4, why am I getting an error related to a deleted copy constructor if I'm deleting a move operator? I'm totally lost on this one, especially because Line 3 didn't have any errors.

dboeger1
  • 21
  • 5
  • 3
    Try to make your example snippet minimal; the enum needs to only have a few members for you to demonstrate the behavior. – cigien Aug 08 '21 at 21:57
  • 1
    Can't reproduce. What compiler? What Standard? What does your *source* (CPP) file look like? – Adrian Mole Aug 08 '21 at 22:11
  • 1
    Can you try defining the static array in the cpp file, not in the header? As suggested here: https://stackoverflow.com/a/2117331/260313 – rturrado Aug 08 '21 at 22:11
  • Is there any particular reason you are constructing the enum member in the constructor initializer list with curly braces instead of parentheses? In other words, why do this `Territory(Identifier identifier) : identifier{identifier} {}` instead of this? `Territory(Identifier identifier) : identifier(identifier) {}` – Joe Aug 08 '21 at 22:16
  • @Joe Why not use the brace initialization syntax? – Ted Lyngmo Aug 08 '21 at 22:27
  • 1
    I'm trying to update my example to make it a bit simpler, but keep in mind I'm following a book and still pretty early on, so I'm not intimately familiar with how to properly refactor all of this code yet. To answer @Joe, the book I'm reading is "C++ Crash Course: A Fast-Paced Introduction" by Josh Lospinoso, and it recommends always using braced initialization. I forget the exact examples, but it provided some examples of edge cases where parentheses didn't work quite right or were a bit unintuitive. I'm trying to develop good habits to start, so braces seemed like the way to go. – dboeger1 Aug 08 '21 at 22:29
  • please read about [mcve]. 2 or 3 enum values would be enough to demonstrate the issue – 463035818_is_not_an_ai Aug 08 '21 at 22:30
  • This code compiles perfectly fine: https://onlinegdb.com/Qeoqpoemm – pelya Aug 08 '21 at 22:38
  • Your error appears to be produced by clang compiling for C++14 or earlier standards. A different, but similar, error message is produced by gcc for those standards. Compiling for C++17 does not produce errors. Which standard do you intend to target? – JaMiT Aug 08 '21 at 23:54
  • @JaMiT That seems likely since somebody else was able to compile without issue. I intend to target the latest standard (C++20 if I'm not mistaken) unless there is some strong reason not to. At the moment, I'm just following a book for learning purposes, so not sure why I would be hitting anything too new syntactically. Now if only I could figure out where the heck the compiler settings are in Xcode... – dboeger1 Aug 09 '21 at 00:00
  • @dboeger1 Does [Many questions about the various C++ compilers available to me on OS X](https://stackoverflow.com/questions/35592195/) help? It's a few years old, so the locations of options might have changed. – JaMiT Aug 09 '21 at 00:02

1 Answers1

1

I believe I've solved my problem, although it unfortunately doesn't really clarify why I was having issues in the first place. But alas, I am not in the business of figuring out why new code doesn't compile on old compilers, so the show must go on.

After some discussion in the comments and someone being able to build and run the code without issue, I dug around in my Xcode project settings and found that it was compiling for C++14. I changed that to C++17, uncommented all 4 lines, and the bulk of the errors went away.

There was 1 remaining error on my for loop line in main.cpp:

Call to deleted constructor of 'TerritoryUSA::Territory'

It also highlights the uncommented Line 1 in gray with this message:

1. 'Territory' has been explicitly marked deleted here

That line is the copy constructor, so I took a wild guess that the range-based for loop was implemented using copy constructors. I quickly added a reference modifier to the line to make it look like this:

    for (Territory& territory : territories) {

and voila, it builds and runs again. I don't even think range-based for loops existed in C++ back when I used the language more regularly, so I didn't know if the reference modifier would work as intended there, but it seems to. Pretty nifty, almost like a lower-level Python without copying.

dboeger1
  • 21
  • 5
  • That is actually not a copy constructor. At least not the one you want. the argument should be `const Territory&`, not `Territory&`. But glad you've solved your problem – Joe Aug 09 '21 at 00:16
  • That was going to be my next question, although I didn't want to over-complicate the issue at hand. I'm still a little fuzzy on when exactly const is necessary. I thought that const only mattered because it would work for r-values as well, but if you provide a move constructor, r-values will use that one instead. Ah well, I don't mean to bug you for all the detail, this is stuff I just need to research and practice with more. Of course I was thrown for a loop by the compiler on my first sample project. – dboeger1 Aug 09 '21 at 00:21
  • It's pretty simple. If the function you are calling (the copy constructory, in this case) should *not* be allowed to change the argument passed in, then it should be const. A copy constructor definitely should not be allowed to change what's given. It *copies* it. Since you pass in the Territory object by reference, that means the constructor has access to the original, underlying object. It's just like passing in a pointer. So if you want to signal to all callers that it will not be changed, mark it `const Territory&` – Joe Aug 09 '21 at 00:26
  • Now if Territory were a really trivial class with semantics that allowed for copying/assignment (which, I gather, it does not). Then you might have a copy constructor that took an argument of just `Territory` (i.e. passing by value which implicitly makes a copy), not a `const Territory&` But you have clearly tried to disallow that, so `const Territory&` it is. – Joe Aug 09 '21 at 00:29
  • I should add that I started C++ back in the 1990s, long before things like R-value references even existed. That's why I asked about the curly-braces instead of parentheses for the constructor-initializer list. My idea of "best practices" is occasionally overcome by language improvements. (So I'm going to need to read up on why that practice is better...) But everything I've written in the two comments above is still technically true – Joe Aug 09 '21 at 00:39