Passer By
Passer By

Reputation: 21160

Interface design: safety of overloaded function taking string and char array

Say we have a class that can write stuff to output

class Writer
{
public:
    int write(const std::string& str);
    int write(const char* str, int len);
    //...
};

I was fine with this, its flexible and all that, until I realized

char* buf = new char[n]; //not terminated with '\0'
//load up buf
Writer w;
w.write(buf);  //compiles!

That is a really nasty bug.

We can amend somewhat with some templating

class WriterV2
{
public:
    int write(const std::string& str);
    int write(const char* str, int len);
    template<typename... Args>
    int write(const char*, Args...)
    { static_assert(sizeof...(Args) < 0, "Incorrect arguments"); }
    //...
};

But this method has its problems

WriterV2 w;
w.write("The templating genius!"); //compile error

What do I do? What is a better design?

And before anyone asks, overloading for const char (&)[N] does not work. It might be feasible to create a wrapper to do this, but that seems... overkill?

EDIT Adding a method write(char*) and emitting an error there is not ideal. When passing buf around through functions and all that, it might become const char*.

Upvotes: 7

Views: 122

Answers (1)

WhiZTiM
WhiZTiM

Reputation: 21576

ICS (Implicit Conversion Sequences) during overload resolution in C++ can produce surprising results as you've noticed, and also quite annoying..

You can provide necessary interfaces you need, then carefully employ templates to handle the string literal vs const char* fiasco by taking advantage of partial ordering to delete the unwanted overload.

Code:

#include <iostream>
#include <string>
#include <type_traits>

class Writer
{
public:
    int write(std::string&&) { std::cout << "int write(std::string)\n"; return 0; }
    int write(const std::string&) { std::cout << "int write(const std::string& str)\n"; return 0; }
    int write(const char*, int){ std::cout << "int write(const char* str, int len)\n"; return 0; }

    template<std::size_t N = 0, typename = std::enable_if_t<(N > 0)> >
    int write(const char (&)[N]) { std::cout << "int write(string-literal) " << N << " \n"; return 0; }


    template<typename T>
    int write(T&&) = delete;

};

int main(){
    char* buf = new char[30];
    const char* cbuf = buf;
    Writer w;

    //w.write(buf);                     //Fails! 
    //w.write(cbuf);                    //Fails! 
    w.write(buf, 30);                   //Ok!    int write(const char*, int);
    w.write(std::string("Haha"));       //Ok!    int write(std::string&&);
    w.write("This is cool");            //Ok!    int write(const char (&)[13]);
}

Prints:

int write(const char* str, int len)
int write(std::string)
int write(string-literal) 13 

Demo


Note that the solution above inherits a disadvantage of "overloading a function with an unconstrained Forwarding Reference". This means that all ICS to the argument type(s) of the viable functions in the overload set will be "deleted"

Upvotes: 5

Related Questions