Home > Blockchain >  Shall I try to use const T & as much as possible?
Shall I try to use const T & as much as possible?

Time:09-01

I am learning books <Effective C >, i think use const reference is a good practice. because it can avoid unnecessary copy.

so, even in initialize a object, i use const T & t = T();

here is the code:

#include <string>
#include <iostream>
#include <vector>

using namespace std;

template <class T>
inline std::vector<std::string> Split(const std::string &str, const T &delim, const bool trim_empty = false) {
  if (str.empty()) return {}; 
  size_t pos, last_pos = 0, len;
  std::vector<std::string> tokens;
  std::string delim_s = ""; 
  delim_s  = delim;
  while(true) {
    pos = str.find(delim_s, last_pos);
    if (pos == std::string::npos) pos = str.size();
    len = pos-last_pos;
    if ( !trim_empty || len != 0) tokens.push_back(str.substr(last_pos, len));
    if (pos == str.size()) break; 
    else last_pos = pos   delim_s.size();
  }   
  return tokens;
}

int main() {
  const std::string& str = "myname@is@haha@";  // compare with std::string = "", will this be better(in speed and memory)?
  const std::string& s = Split(str, "@").front();  // this crashed
  // std::string s = Split(str, "@").front();  // this ok
  cout << "result is " << s << endl;
}

As you see above, this code is used to split a string into vector<std::string>,

in main function:

I have two method to get the first element of the splited results.

1.const std::string& s = Split(str, "@").front();

2.std::string s = Split(str, "@").front();

in my test, option 1 will crashed, option2 is ok.

could you talk some difference of these?

and is it necessary to do this (const std::string& s = "asd";)?

comparing to std::string s = "asd", what is the advantages and disadvantages of them?

CodePudding user response:

const std::string& str = "myname@is@haha@";

This works due to reference lifetime extension. When a prvalue is bound immediately to a reference-to-const or an rvalue reference its lifetime gets extended to the lifetime of the reference. Since "myname@is@haha@" is not a std::string, a temporary std::string gets constructed to hold that value. That temporary is a prvalue, and so is eligible for lifetime extension.

const std::string& s = Split(str, "@").front();

This doesn't work because std::vector::front doesn't return a prvalue, it returns an lvalue. Reference lifetime extension does not apply in this case. The object referenced by the reference returned by front is part of the std::vector returned by Split, and dies along with it at the end of the expression leaving s dangling.


This part is wandering a bit into opinion, but in general if you want an object, just declare that you have an object. There are far fewer pitfalls, and given modern compiler optimizations there is no performance benefit to relying on reference lifetime extension. Anywhere you may have saved a copy by relying on reference lifetime extension, copy elision (guaranteed in C 17 and above, widely applied by compilers prior to that) will save the copy for you.

CodePudding user response:

Someone has to own data. C , like many low-level languages, fancies itself as a single-ownership language in general (there are things like std::shared_ptr that behave differently, but the default for scalar values is single-ownership). That means that someone, somewhere, has to actually store that data. That might be a structure or class, that might be a local variable in a function, or it might be the static memory of your program itself. Once someone owns the data, then, and only then, can you take references to it.

const std::string& s = Split(str, "@").front();

Split(str, "@") returns a std::vector. By value, which means the caller of Split is taking ownership of that value. Now, that std::vector is a temporary value and, unless we do something to change it, it will disappear at the end of the current expression (basically, when we hit the semicolon). So we call front(), which returns a reference to data in the vector. Then we destroy the vector. It's gone, forgotten about, never existed. But we have a reference to it, so that reference is now garbage. The problem is that no one owns the vector anymore, so it's gone. On the other hand,

std::string s = Split(str, "@").front();

Here, we copy the std::string out of the std::vector before destroying it. The new string has no actual ties to the vector, it merely looks just like a value in the vector, so when the temporary std::vector is destroyed, it doesn't affect us.

If you're really intent on avoiding copies, you can take ownership of the vector as a local variable. I think this is overkill for std::string, but if your data structure was incredibly difficult (or even impossible) to copy, this might be warranted.

std::vector<std::string> my_vec = Split(str, "@");
const std::string& s = my_vec.front();
// s is good until my_vec goes out of scope, then it's garbage.

With the string literal, it's straight-up undefined behavior.

const std::string& s = "asd";

"asd" is a string literal, so the character array representing those four characters (three letters plus a null terminator) has static lifetime, which means it won't be freed till your program ends. But you didn't take a reference to a character array. You took a reference to an std::string, so what you did was actually

const std::string& s = std::string("asd");

And there's a temporary object being constructed. Then we take a reference and then immediately free the referenced object, same as before. The fact that that works at all (and it does work on my machine, at least in a trivial example) is happenstance; the memory that was being used for that std::string hasn't been reused for something else yet.

  • Related