I am studying the code of an open source app. I have made a simpler version of this code to isolate something bugging me (though I have several questions with this code that I am hoping C gurus will be able to help me with, I will start with the main one).
Main question: why do I need an "empty" constructor (no arguments) for the class of an object that's being assigned to a std::map?
The main idea (see code below) is to assign an instance of the Variant class to a std::map
(whose key is a std::string
). Here is the code:
#include <iostream>
#include <map>
#include <string>
#include <memory>
struct Data
{
public:
Data(const void* data, size_t bytes) : bytes(bytes)
{
ptr = malloc(bytes);
memcpy(ptr, data, bytes);
std::cout << "in ctor of Data" << std::endl;
}
~Data() { free(ptr); std::cout << "in dtor of Data" << std::endl; }
void* ptr{ nullptr };
size_t bytes;
};
struct DataStream
{
public:
DataStream(const std::shared_ptr<Data>& ptr, size_t size) : ptr(ptr), size(size)
{ std::cout << "in ctor of DataStream" << std::endl; }
std::shared_ptr<Data> ptr;
size_t size;
};
struct Variant
{
public:
enum Type
{
EMPTY,
TYPE1 = 5,
TYPE2 = 10
};
~Variant() { std::cout << "in dtor of Variant" << std::endl; }
// XXX If this ctor does NOT exist, the code doesn't compile XXX
Variant() : type(EMPTY) { std::cout << "in ctor of Variant" << std::endl; }
Variant(const int& n, Type type) : n(n), type(type) {}
Variant(const std::shared_ptr<Data>& data, Type type, size_t size) : type(type), data(std::make_shared<DataStream>(data, size))
{ std::cout << "in ctor of Variant (ptr to typed array)" << std::endl; }
Type type;
int n;
std::shared_ptr<DataStream> data;
};
struct Params
{
public:
void add(const std::string& name, const Variant& data) { params[name] = data; }
const Variant& operator[] (const std::string& name) { return params[name]; }
std::map<std::string, Variant> params;
};
struct Handle
{
public:
Params params;
void set(const std::string& name, const Variant& data) { params.add(name, data); }
};
int main()
{
Handle* handle = new Handle();
char data_i[3] = { 'a', 'b', 'c' };
std::shared_ptr<Data> data = std::make_shared<Data>(data_i, 3);
handle->set("testC", Variant(data, Variant::TYPE1, 3));
std::cout << "use_count data " << handle->params["testC"].data->ptr.use_count() << std::endl;
std::cout << "Variant type " << handle->params["testC"].type << std::endl;
delete handle;
return 0;
}
If I don't add to the class a constructor that doesn't take any arguments (what I call an empty constructor) the code doesn't compile. I get the following error msg:
test3.cpp:52:68: note: in instantiation of member function 'std::map<std::basic_string<char>, Variant>::operator[]' requested here
void add(const std::string& name, const Variant& data) { params[name] = data; }
^
test3.cpp:29:8: note: candidate constructor (the implicit copy constructor) not viable: requires 1 argument, but 0 were provided
struct Variant
^
test3.cpp:40:5: note: candidate constructor not viable: requires 2 arguments, but 0 were provided
Variant(const int& n, Type type) : n(n), type(type) {}
^
test3.cpp:41:5: note: candidate constructor not viable: requires 3 arguments, but 0 were provided
Variant(const std::shared_ptr<Data>& data, Type type, size_t size) : type(type), data(std::make_shared<DataStream>(data, size))
^
My understanding of what's going on is limited here. I do get that I am assigning an instance of the class Variant to a map and that an instance has to be created at this point of the code. Therefore the ctor of the object's class will be called. Makes sense. And since it has no argument it needs to use a constructor that does not take any argument. Fair enough. So I add the line for the constructor with no argument. The code compiles. Then, I output the type of the object stored in the map and I can see that even though the constructor with no argument was called for the creation of that object, it still has the type of the original object (5 rather than 0).
So this is the bit for which I'd like to have an explanation. How can it actually copy the content of the original object (the one that's created in main()
) even though the constructor with no argument when assigning a copy of that object to std::map
was used?
Subsidiary question #1 ):
If I look at the sequence of constructor/destructor for that Variant object I get the following:
in ctor of Data
in ctor of DataStream
in ctor of Variant (ptr to typed array)
in ctor of Variant
in dtor of Variant
use_count data 2
Variant type 5
in dtor of Variant
in dtor of Data
In the original code, the add
method is:
void add(const std::string& name, Variant data) { params[name] = data; }
The const ref is not even there. And the sequence is:
in ctor of Data
in ctor of DataStream
in ctor of Variant (ptr to typed array)
in ctor of Variant
in dtor of Variant
in dtor of Variant
use_count data 2
Variant type 5
in dtor of Variant
in dtor of Data
The destructor of Variant is called 3 times but the constructors only twice! Not sure which constructor I am missing in this case.
But anyway, my question is: from the moment I create the temporary variable here:
handle->set("testC", Variant(data, Variant::TYPE1, 3));
Can I somehow insure that no copies of that object are made until I assign it to map? I have tried to add a bunch of std::move
there & there, but it doesn't seem to make a difference. I am just thinking that these copies are not necessarily mandatory and that there must be a way of avoiding them. Your guru's input would be greatly appreciated for these 2 questions.
CodePudding user response:
operator[]
requires that the type is DefaultConstructible
, if the key does not exist.
On line params[name] = data;
operator[]
creates an element using the default constructoroperator[]
returns a reference to the elementdata
is assigned to the reference, using the copy constructor
In your case, the step 1 fails because there is no default constructor.
C 17 adds insert_or_assign()
, which does not require the type to be DefaultConstructible
.