Skip to content

Understanding qAsConst and std::as_const

Every now and then, when I submit some code for a code review, people tell me that I forgot qAsConst.

Now I have one more enemy, namely: Clazy! It has also started saying this, and I guess it’s about time for me to figure out what is going on. When do I need qAsConst and why do I need to know these things?

The Dreaded Warning

The following code is an example of where Clazy and my coworkers tell me to add qAsConst:

class MyClass {
  QStringList m_list;
public:
  void print() {
     for (const QString &str : m_list)  // WARNING: might detach, add qAsConst
        print(str);
  }
};

Why do I need qAsConst there? Let me tell you a little bit about what’s happening behind the scenes, and that will allow us to understand why qAsConst is needed.

Clazy is complaining there regarding a very peculiar interaction that we have between the C++11 range-based for loop that we have there and a Qt container, in this case QStringList. But this can apply to any Qt container.

What is the (potential) problem with that code that Clazy is warning about? Clazy is warning that the code, written like that, could perform a hidden copy of your container. If you just take your container and run through the container, there’s no copy in here. It looks like there is no copy, but there could be a copy. Why would you need a copy here?


It all boils down to the fact that Qt containers are implicitly shared or, as we say, Copy On Write (or COW 🐮). In a nutshell, that means that inside each and every Qt container there is a little number — a reference counter. Every time you try to modify a container, the first thing that Qt does is checks that number.

If the number is bigger than 1, then Qt does a deep copy of that container. We refer to this operation as a detach of the container. That code, written like that, could detach. That’s the kind of warning that Clazy is giving you. Since it looks like that, you’re not modifying the container in any way. You’re just iterating over the StringList, just printing the strings.

Clazy is getting a bit suspicious, thinking, “Okay, so you’re taking a copy of the container but you don’t want to modify the container? Why are you taking the copy in the first place?”


Where on this particular line does it even have a chance to detach? It’s not visible, but it’s behind the scenes.

It has to do with the new for loop. How is it implemented? Or even better, what is it equivalent to? Do you know what the new for loop expands to when the compiler sees that and compiles it?

For loops on containers have always been about iterators. You get an iterator pointing to the first element, an iterator beyond the last element, and then you iterate through. You’d assume some iterators and then whatever you’re pointing to is copied into your reference, or you’d get a QString reference to that element that it’s pointed to.

How does the range-based for loop iterate on an arbitrary container? You’ve guessed it right, it still uses iterators, even if they’re not visible to us. Given a container, the new for loop calls begin() and end() on it, in order to get the iterators that are necessary to actually transverse the container.

Here’s the catch: if the container is a Qt container, and it is non-const, then those operations are operations that do the little game I was playing; that is, they go inside the Qt container, check if the reference counter is bigger than 1, and, if it is, detach.

So the very act of calling begin() and end() could detach. As I said, it’s not visible but it’s there, hidden inside the range-based for loop.

How to Solve the Problem

Make the Container const

When using a Qt container, a necessary condition for a detach to happen is that we are calling methods such as begin() and end() on a container which is not const. Therefore, the simplest approach to fix the whole problem is to make that container const somehow. We could, for instance, take a copy — a const copy, of course, or even better, a const reference to the container.

class MyClass {
  QString m_list;
public:
  void print() {
     const QStringList copy = m_list;
     for (const QString &str : copy) // OK
        print(str);
  }
};

Here we’re taking a const copy of the container. Now, taking a copy is cheap, given this is a Qt container. The copy on write mechanism implies that the act of taking a copy simply increments the reference counter. (If you take a const reference, you’re not even paying for that.)

But now the key aspect is that the for loop is acting on a const container: the begin() that it’s going to call is going to be the begin() overload for const containers, which does not detach. Since you have a const container, there’s no chance for you to modify the container because it’s const. So there is no need to detach at all. And, indeed, this will suppress the warning from Clazy because you cannot detach anymore.

There are better ways, of course. There are a couple of other things that you can do.

Mark the Entire Method As const

The first thing you can do better is realize that the entire print function actually doesn’t need to modify this object. So you could mark the entire method as const and that would, as a by-product, make the member m_list const. So, you’re good to go.

class MyClass {
  QStringList m_list;
public:
  void print() const {
     for (const QString &str : m_list) 
        print(str);
  }
};

That would be the best solution. But, of course, it’s not general — maybe you also need to do something else and modify this, so you cannot mark a given method as const.

qAsConst or std::as_const

The second best solution is using something like qAsConst or, equivalently, std::as_const on top of your m_list object.

class MyClass {
  QStringList m_list;
public:
  void print() {
     for (const QString &str : std::as_const(m_list))
        print(str);
  }
};

qAsConst and std::as_const do exactly the same thing. std::as_const is C++17, so if you’re not on 17 just yet, you can use the Qt version. What they do is simply give you back a const reference to that container.

What do these functions do? They’re just like a const_cast — they’re taking a reference to m_list and adding a const on top of it. So it’s just a manipulation of the type, something that’s done just for the purpose of the compiler. Now the compiler knows that the thing it gets out of qAsConst is, of course, a reference to a const QStringList; so it can call only the const begin() and the const end(). As we have discussed, those calls will not detach your container.

This solution doesn’t cost you anything at runtime. It does not generate any additional code or anything of that sort. It is purely a type manipulation done for the compiler.

Corner Cases

There are some corner cases worth discussing.

Suppose that you have a method that generates a container and you call that method from a range-based for loop because you want to iterate over its results:

QStringList generateStrings();

void print() {
   for (const QString &str : generateStrings()) // Clazy warning here
      print(str);
}

Of course Clazy is going to give you a warning there, because you could be detaching the container returned by the function call. So you try to wrap the call in qAsConst:

QStringList generateStrings();

void print() {
  for (const QString &str : qAsConst(generateStrings())) // ERROR
    print(str);
}

This code doesn’t compile. The reason is a bit complicated, because it has to do with the C++ rules around what exactly you can feed into qAsConst. To keep it simple, you’re not allowed to place things like function calls inside qAsConst or, equivalently, inside std::as_const. It’s actually a good thing that you can’t do that because, otherwise, your program would crash at runtime.

Declare a Local QStringList

Fortunately, for this specific use case, there is a better solution. You can simply declare a local QStringList. Make it const, of course, because that’s the purpose of the whole exercise. Then, pass the local QStringList into your for loop. For instance:

QStringList generateStrings();

void print() {
  const QStringList list = generateStrings();
  for (const QString &str : list) // OK
    print(str);
}

In C++20, I would actually not even need an extra line to declare list. A nice thing about C++20, as a kind of smaller convenience, it added the possibility of adding an initialization inside the range-based for loop. This brings it slightly closer to the old for loop, when you always had the first statement, then the guard, and then the increment.

So you could write something like this, to spare you having this variable around for longer than the loop itself:

QStringList generateStrings();

void print() {
  for (const QStringList list = generateStrings(); const QString &str : list) // OK
    print(str);
}

Why does this problem exist to begin with? What if generateStrings() generates and returns a list and there can’t be anyone else who has that string list?

QStringList generateStrings() {
  QStringList result;
  result << "hello" << "world";
  return result;
}

void print() {
   for (const QString &str : generateStrings()) // Why a warning? The returned list isn't shared with anyone!
      print(str);
}

You, as a human being, can reason on the code flow and see that the QStringList returned by generateStrings() is not shared. Its reference counter is going to be 1, and that cannot detach. But that’s why this is a warning: Clazy is just telling you that this could detach. You have to remember that, in the general case, your generateStrings() function could not be returning a brand new QStringList.

Maybe it’s returning a QStringList that you have elsewhere. If that’s the case, that return from generateStrings() would implicitly increase the reference counter of your QStringList, because you’re creating a new copy. You would actually detach.

Inside your for loop, you would have a QStringList with a reference counter bigger than 1. Clazy, of course, does not have a crystal ball; it cannot predict what your code is going to do. So it’s just warning you by telling you this code might detach — that there is the possibility. It is actually a fairly concrete possibility, given the fact that, typically, when dealing with Qt containers, we always return them by value.

Even if doesn’t actually detach right now, one day we might refactor the code. We may say something like, “my string list has to survive the generateStrings() method. Maybe I want to cache it. Maybe I want to save that work”. That day, the reference counter is going to increase and the warning would actually be a good warning.

Return a const QStringList?

Instead of just returning a QStringList, generateStrings() could return a const QStringList:

const QStringList generateStrings();

void print() {
   for (const QString &str : generateStrings()) // OK
      print(str);
}

Clazy will not complain here, again because we cannot detach a const container.

However: this is a bad idea. You shouldn’t be returning const things from functions, and especially const containers. Why is that? Because you’re going to break move semantics.

C++11 introduced move semantics, which allow you to optimize, in general, the return from functions. When you call a function and get a value back, you’re going to get a temporary built as result of the function call. You can then move from that temporary into, let’s say, a local object. Moving containers is usually very cheap! That’s definitely something we want to keep.

If you return const objects however that move gets broken; you can’t move from a const object. What happens then? You’re going to perform a copy instead. Now, in the case of Qt containers, you can get away with that because copying a Qt container is not particularly expensive. All that you really do is increase the reference counter.

So, as a practical workaround, that could work. But consider this: the moment you decide you don’t like QStringList any more and decide to make it a std::vector or something more complicated (some non-Qt container of any sort), you’re going to pay for that const because copying a standard container and not moving it is going to cost you a lot. So it’s very important to be careful about what you do.

Return a std::vector

You wouldn’t have this problem at all with std::vector. The reason for that is that std::vector, and actually all Standard Library containers, are not reference counted; they do not implement this mechanism. Among these other things, this means that a call to begin() into a std::vector object will never copy it behind the scenes. You’ll be absolutely fine, if your generateStrings() function returns a std::vector.

std::vector<QString> generateStrings();

void print() {
   for (const QString &str : generateStrings()) // OK
      print(str);
}

Summary

To summarize, you need qAsConst() when you iterate in a range-based for loop. That’s the first thing. And you need qAsConst when what you’re iterating over is non-const.

It could be const if you’re in a const method (and the variable is a data member of this) or if it’s a variable that you declared as const elsewhere. Otherwise, you need qAsConst but you can only use qAsConst if it’s an l-value, something that has a local name, and not the return of a function.

If you are iterating over an object that you get from a function call, save it in a local variable instead (and make it const).

Mutating Iteration

The Clazy warning stems from the fact that your for loop doesn’t want to modify your container. In all of our examples, we were using just a debug, or it seemed like we were just reading from the container. Clearly, if you do want to modify the container, then you don’t want to apply const at any level! You would need to be working on a non-const container.

But the key aspect of mutating iteration is that, in this case, your iteration variable would have a different type. As you can see, right now we’re taking a const QString reference. That means that we cannot mutate the container contents through that variable. If you need to modify the container itself, you’d be taking a non-const QString reference! In that case, you would not get the warning from Clazy, because it would detect that you want to modify the container. If that means a detach, that’s the price to pay — you have a container that’s just a shallow copy to some shared data and, if you want to modify it, you need to create your own copy of that data.

In C++23 there is yet another solution that will work in even more cases. We’ll cover that at a later time, either on our YouTube channel or as a blog, or both. We’ll keep you posted, so please stay tuned.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

FacebookTwitterLinkedInEmail

Categories: C++ / KDAB Blogs / KDAB on Qt

Tags: / / /

6 thoughts on “Understanding qAsConst and std::as_const”

  1. Thanks for the interesting article… took me a while to follow though as I think MyClass:m_list needs to be a QStringList (rather than QString), otherwise the loop doesn’t read properly. Also the HTML “&” is not getting translated as expected, further reducing the readability.

    1. Giuseppe D'Angelo

      Hi,

      Qt’s foreach has different semantics than a range-based foor loop. For instance, you cannot mutate the container’s elements when using Qt’s foreach. It’s also got even more downsides — they’re discussed here https://www.kdab.com/goodbye-q_foreach/

      Right now, foreach is not officially deprecated yet, but it’s definitely not recommended practice.

  2. How about this? Wouldn’t it help?

    QStringList generateStrings();
    
    template <typename T>
    inline const T moveToConst(T &&t)
    {
        return std::move(t);
    }
    
    void print() {
       for (const QString &str : moveToConst(generateStrings()))
          print(str);
    }
    
    1. Giuseppe D'Angelo

      Hi,

      Yes, that solution works. I am very conflicted about promoting it.

      First, note that you’re taking a forwarding/universal reference, and unconditionally moving from it (indeed you named the function moveToConst). This means that one shouldn’t use it on lvalues that one wants to keep — one should stick to std::as_const for those.

      This is now adding mental burden: you have to remember to use different tools in different circumstances, when the end goal is exactly the same, just to cope with the idiosincrasies of C++ value categories.

      One could develop a more elaborated strategy:

      template <typename T>
      // lvalues
      const T &make_const(const T &t) { return t; }
      
      // rvalues -- need to make it not work with the universal reference
      template <typename T, std::enable_if_t<!std::is_reference_v<T>, bool> = true>
      const T make_const(T &&t) { return std::move(t); }
      
      for (const QString &s :    make_const(stringList)      ) { print(s); } // stringList is not moved-from
      for (const QString &s :    make_const(getStringList()) ) { print(s); } // return of getStringList() moved into make_const's return object
      

      This is now slightly more general. But I’m still not 100% convinced. For instance, suppose you want to give your const container to an adaptor. You create the adaptor and pass to it the result of make_const(container). Since now the adaptor is acting on a const container, the adaptor may need to copy from it, rather than moving from it (effectively, again, one is preventing moves). That’s maybe all fine because Qt containers are cheap to copy (except QVLA! except QFlatMap! except…), but again, what happens if you refactor some code and switch from QVector to std::vector? You now have an expensive copy there, maybe without even noticing that a copy is taking place.

      I’m afraid that The One Solution To Rule Them All™ will come with C++23:

      for (const QString &s :    stringList   | views::as_const) { print(s); }
      for (const QString &s : getStringList() | views::as_const) { print(s); }
      for (const QString &s :        vector   | views::as_const) { print(s); }
      for (const QString &s :     getVector() | views::as_const) { print(s); }
      
Leave a Reply

Your email address will not be published. Required fields are marked *