What is better implicit conversion through constructor or explicit function in this case?
Asked Answered
O

1

6

I am creating my own class for String using C++ solely for learning purposes.

And I stuck upon the place where I should make a decision. Let me explain the matter.

I have two options of my class. I will post below only relevant pieces of the code because I do not want to distract you from my problem at hand. If in order to help me out you need more info I will gladly provide it.

Option 1

class String {
    size_t _length;
    char* _stringHead;
public:
    String(const std::string&);
    String(const char*);
    String(const char);
};
String operator+(String, const String);

const bool operator==(const String, const String);
const bool operator!=(const String, const String);
const bool operator<(const String, const String);
const bool operator<=(const String, const String);
const bool operator>(const String, const String);
const bool operator>=(const String, const String);

Option 2

class String {
    size_t _length;
    char* _stringHead;
public:
    //irrelevant part of code in Option 2
    String(const std::string&);
    String(const char*);
    String(const char);
    //irrelevant part of code in Option 2
};
String operator+(String, const String&);

const bool operator==(const String&, const String&);
const bool operator!=(const String&, const String&);
const bool operator<(const String&, const String&);
const bool operator<=(const String&, const String&);
const bool operator>(const String&, const String&);
const bool operator>=(const String&, const String&);

//for std::string
String operator+(String, const std::string&);

const bool operator==(const String&, const std::string&);
const bool operator!=(const String&, const std::string&);
const bool operator<(const String&, const std::string&);
const bool operator<=(const String&, const std::string&);
const bool operator>(const String&, const std::string&);
const bool operator>=(const String&, const std::string&);

String operator+(const std::string&, String);

const bool operator==(const std::string&, const String&);
const bool operator!=(const std::string&, const String&);
const bool operator<(const std::string&, const String&);
const bool operator<=(const std::string&, const String&);
const bool operator>(const std::string&, const String&);
const bool operator>=(const std::string&, const String&);
//for std::string

//the same goes for char* and char
...
//the same goes for char* and char

So, as you can see from the Option 1 and Option 2 specs the decision here is about whether to use implicit type conversion which is done with the help of constructors or to type each utility separately for each type with which I want my String type to work.

As far as I can see right now the benefit of using the first approach is that it is easier to implement and maintain. While the second approach may produce better performance results.

I would like to get constructive arguments which approach is better and in what scenarios and which approach hence would you use. I think that the biggest part I am interested here is whether or not the performance benefit of the second approach is reasonable.

Overblouse answered 20/9, 2018 at 18:31 Comment(8)
Since this is solely for learning purposes, do it both ways and run some performance tests (timing) to see which gives better performance.Purge
@Marker, the numbers here are very low. The results won`t be useful at all. I would like to know what approach would be used in a real life.Overblouse
@hellohowdoyoudo, Agree, you would have to do it in a loop with 1000s of iterations. Sometimes I do that just to satisfy my own curiosity. If the difference is negligible, I stick with the simpler implementation.Purge
const bool operator==(const String, const String); creates copies of the String parameters which are of arbitrary length. See std::string::operator+ for comparison: en.cppreference.com/w/cpp/string/basic_string/operator%2BTantalize
Bear in mind that any constructor taking a single parameter can be seized upon by the compiler and used to silently convert an object to another type. Typically this happens when you try to pass the wrong type of parameter to a function. The presence of the implicit conversion can make it considerably harder to spot your mistakes. That's not a definitive argument, by any means, but it is (in my opinion) a strong argument for making all of your "conversion" constructors explicit.Chacha
what do you mean with "//irrelevant part of code in Option 2" ? It is not really irrelevant if there is a conversion from std::string to String. Did you mean to remove the conversion from Option 2?Noted
@Overblouse I recommend Quick C++ Benchmark for microbenchmarking. It can measure very low running times. For instance, see quick-bench.com/LXeHNjtCLFtB-Ld1A_tLK9J3cKo, where the effects of small string optimization are measured on the creation/destruction of a single object.Gaggle
@user463035818, yes I did mean this.Overblouse
W
2

An implicit constructor is used to create an instance of the class type when passing an instance of the parameter type to a method that expects the class type. This implicit conversion is done by calling a constructor of the class.

For example, run this code which is similar to yours:

#include <iostream>

class String {
 public:
  String(const std::string& s) {
    std::cout << "called" << std::endl;
  };
};

std::ostream& operator<< (std::ostream& stream, const String& s) {
  return stream;
}

void hello(String s) {
  std::cout << "Hello " << s; // Outputs "called" before "Hello ".
}

int main() {
  std::string s = "world";
  hello(s); // Uses the implicit conversion constructor.
}

Because a new instance of the class String has to be created every time, it is to be expected that performance will suffer slightly. But, in my opinion, not enough to outweigh the benefits: implicit conversion can greatly simplify the job of a class designer and make using a class easier.

However, keep in mind that there exist situations where team members are more likely to be surprised if a conversion happens automatically than to be helped by the existence of the conversion.

Here is such an example:

#include <iostream>

class String {
 public:
  String(int size) {};
};

std::ostream& operator<< (std::ostream& stream, const String& s) {
    return stream;
}

void hello(String s) {
  std::cout << "Hello " << s; // Prints "Hello " as no error occurs.
}

int main() {
  hello(10); // It still calls the implicit conversion constructor.
}

In the code above, an error message produced by the explicit keyword could save someone a bit of time spent debugging.

Some circumstances in which implicit conversion makes sense are:

  1. The class is cheap enough to construct that you don't care if it's implicitly constructed.
  2. Some classes are conceptually similar to their arguments, such as std::string reflecting the same concept as the const char* it can implicitly convert from, so implicit conversion makes sense, as is the case here.
  3. Some classes become a lot more unpleasant to use if implicit conversion is disabled. Think of having to explicitly construct a std::string every time you want to pass a string literal.

Some circumstances in which implicit conversion makes less sense are:

  1. Construction is expensive.
  2. Classes are conceptually very dissimilar to their arguments. Consider the example with the String and the int.
  3. Construction may have unwanted side effects. For example, an AnsiString class should not implicitly construct from a UnicodeString, since the Unicode-to-ANSI conversion may lose information.

So, my advice in your particular case would be to use conversion because it makes sense, as your class is so similar to std::string and to minimize code duplication, but in the future, use implicit conversion with thought.

Witwatersrand answered 20/9, 2018 at 19:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.