Klypto
Klypto

Reputation: 523

more than one operator "[]" matches these operands

I have a class that has both implicit conversion operator() to intrinsic types and the ability to access by a string index operator[] that is used for a settings store. It compiles and works very well in unit tests on gcc 6.3 & MSVC however the class causes some ambiguity warnings on intellisense and clang which is not acceptable for use.

Super slimmed down version: https://onlinegdb.com/rJ-q7svG8

#include <memory>
#include <unordered_map>
#include <string>


struct Setting
{
    int data; // this in reality is a Variant of intrinsic types + std::string
    std::unordered_map<std::string, std::shared_ptr<Setting>> children;
    template<typename T>
    operator T()
    {
        return data;
    }
    template<typename T>
    Setting & operator=(T val)
    {
        data = val;
        return *this;
    }
    Setting & operator[](const std::string key)
    {
        if(children.count(key))
            return *(children[key]);
        else
        {
            children[key] = std::shared_ptr<Setting>(new Setting());
            return *(children[key]);
        }
    }
};

Usage:

    Setting data;
    data["TestNode"] = 4;
    data["TestNode"]["SubValue"] = 55;
    int x = data["TestNode"];
    int y = data["TestNode"]["SubValue"];
    std::cout << x <<std::endl;
    std::cout << y;
    
output:
4
55

Error message is as follows:

more than one operator "[]" matches these operands:

built-in operator "integer[pointer-to-object]" function

"Setting::operator[](std::string key)"

operand types are: Setting [ const char [15] ]

I understand why the error/warning exists as it's from the ability to reverse the indexer on an array with the array itself (which by itself is extremely bizarre syntax but makes logical sense with pointer arithmetic).

char* a = "asdf";
char b = a[5];
char c = 5[a];
b == c

I am not sure how to avoid the error message it's presenting while keeping with what I want to accomplish. (implicit assignment & index by string)

Is that possible?

Note: I cannot use C++ features above 11.

Upvotes: 5

Views: 1881

Answers (1)

Indiana Kernick
Indiana Kernick

Reputation: 5331

The issue is the user-defined implicit conversion function template.

template<typename T>
operator T()
{
    return data;
}

When the compiler considers the expression data["TestNode"], some implicit conversions need to take place. The compiler has two options:

  • Convert the const char [9] to a const std::string and call Setting &Setting::operator[](const std::string)
  • Convert the Setting to an int and call const char *operator[](int, const char *)

Both options involve an implicit conversion so the compiler can't decide which one is better. The compiler says that the call is ambiguous.

There a few ways to get around this.

Option 1

Eliminate the implicit conversion from const char [9] to std::string. You can do this by making Setting::operator[] a template that accepts a reference to an array of characters (a reference to a string literal).

template <size_t Size>
Setting &operator[](const char (&key)[Size]);

Option 2

Eliminate the implicit conversion from Setting to int. You can do this by marking the user-defined conversion as explicit.

template <typename T>
explicit operator T() const;

This will require you to update the calling code to use direct initialization instead of copy initialization.

int x{data["TestNode"]};

Option 3

Eliminate the implicit conversion from Setting to int. Another way to do this is by removing the user-defined conversion entirely and using a function.

template <typename T>
T get() const;

Obviously, this will also require you to update the calling code.

int x = data["TestNode"].get<int>();

Some other notes

Some things I noticed about the code is that you didn't mark the user-defined conversion as const. If a member function does not modify the object, you should mark it as const to be able to use that function on a constant object. So put const after the parameter list:

template<typename T>
operator T() const {
    return data;
}

Another thing I noticed was this:

std::shared_ptr<Setting>(new Setting())

Here you're mentioning Setting twice and doing two memory allocations when you could be doing one. It is preferable for code cleanliness and performance to do this instead:

std::make_shared<Setting>()

One more thing, I don't know enough about your design to make this decision myself but do you really need to use std::shared_ptr? I don't remember the last time I used std::shared_ptr as std::unique_ptr is much more efficient and seems to be enough in most situations. And really, do you need a pointer at all? Is there any reason for using std::shared_ptr<Setting> or std::unique_ptr<Setting> over Setting? Just something to think about.

Upvotes: 5

Related Questions