Olly
[ RSS | ATOM 1.0 ]
Powered by PyBlosxom

Home

Empty Strings in C++

Gaurav (one of Xapian's GSoC students this year) queried my advice that it's better not to explicitly initialise a std::string in C++ like this:

std::string s = "";

but instead to write:

std::string s;

It could be argued that the former makes it clearer that the string is initialised (since C++ does inherit C's behaviour of not implicitly initialising variables of fundamental types, such as int and double). But objects aren't left uninitialised - the default constructor gets called (or if there isn't one, the code won't compile).

The downside is that you get quite a lot more code from the first version than the second. Perhaps compilers will grow smarter and in future both the above will compile to the same code, but that's not true today.

Here's a simple bit of test code:

#include <string>

std::string f() {
#ifdef EXPLICIT_INIT
    std::string s = "";
#else
    std::string s;
#endif
    return s;
}

And I'll compile it with GCC (g++ (Debian 4.7.2-4) 4.7.2) on x86-64 to produce assembler output:

$ g++ -O2 -S s.cc -o s1.s
$ g++ -DEXPLICIT_INIT -O2 -S s.cc -o s2.s
$ diff s1.s s2.s
1a2,4
>   .section        .rodata.str1.1,"aMS",@progbits,1
> .LC0:
>   .string ""
9,10c12,25
<   movq    %rdi, %rax
<   movq    $_ZNSs4_Rep20_S_empty_rep_storageE+24, (%rdi)
---
>   pushq   %rbx
>   .cfi_def_cfa_offset 16
>   .cfi_offset 3, -16
>   movl    $.LC0, %esi
>   movq    %rdi, %rbx
>   subq    $16, %rsp
>   .cfi_def_cfa_offset 32
>   leaq    15(%rsp), %rdx
>   call    _ZNSsC1EPKcRKSaIcE
>   addq    $16, %rsp
>   .cfi_def_cfa_offset 16
>   movq    %rbx, %rax
>   popq    %rbx
>   .cfi_def_cfa_offset 8

(As an aside, I wouldn't recommend spending a lot of time digging into the assembler your compiler produces, but if there's more than one equivalent way to write code for a common case (as here) and you want to know which is most efficient, it can be informative to see what code is produced for each).

You don't really need to know x86-64 assembler to grasp what's happening above, which is lucky, since I don't really know x86-64 assembler. In the first hunk, we get an empty literal string being added to the rodata (read-only data) section; and in the second, instead of two instructions which copy a standard empty string representation, we get much more elaborate code, including a function call to _ZNSsC1EPKcRKSaIcE - the C++ mangled name for the std::string from const char * constructor:

$ c++filt _ZNSsC1EPKcRKSaIcE
std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)

I also tested the example above with clang (Debian clang version 3.0-6 (tags/RELEASE_30/final) (based on LLVM 3.0)) and the resultant code is fairly similar for each case.

The overhead for doing this once isn't going to matter, but if it happens every time you declare a std::string variable, the cumulative effects may be measurable. And as well as taking longer to execute, the larger code will cause greater CPU cache pressure.


Posted in coding by Olly Betts on 2013-01-06 01:26 | Permalink


Home