string vs. const(char)[] on the function signature

Ali Çehreli acehreli at yahoo.com
Fri Jul 13 10:19:34 PDT 2012


tl;dr - Please see the conclusion section below.

Although the question is valid for other reference types as well, I will 
only use strings in this post.

The question is whether a function should take a string parameter as 
'string' or as 'const(char)[]'.

string on the function API is a restriction: Such a function wants the 
caller to provide an immutable string:

void foo(string s)  // <-- too restrict
{
     // ... doesn't mutate s ...
}

void main()
{
     char[] s;
     foo(s);  // <-- ERROR: not immutable
}

The caller can copy the string before calling the function but that 
would only be to satisfy the limitation that the immutable parameter brings.

So, a better parameter type is 'const char[]' (or const(char)[]) because 
it can be bound to both mutable and immutable:

void foo(const char[] s)  // <-- welcoming
{
     // ... doesn't mutate s ...
}

void main()
{
     char[] s;
     foo(s);               // <-- works

     foo("");              // <-- works
}

So far so good.

Unfortunately, a problem arises when foo() decides to make a call to a 
function that really (or unnecessarily) needs an immutable string:

void bar(string s)  // <-- really needs immutable
{
     // ...
}

void foo(const char[] s)
{
     // ... doesn't mutate s ...

     bar(s);  // <-- ERROR: not immutable
}

A solution is to make an immutable copy of the string before calling 
bar(), but that would be the same inefficiency as how the original 
caller could make a copy before calling foo():

import std.conv;

void foo(const char[] s)
{
     // ... doesn't mutate s ...

     bar(to!string(s));  // <-- sometimes an unnecessary copy
}

Although to!string is a no-op when the original string is immutable, 
there will always be a copy above because the 'const char[]' (or 
const(char)[]) parameter erases the mutability attribute of the string. 
The compiler can't know whether the original string has been mutable or 
immutable.

In order to retain the mutability information, one can think of inout. 
Unfortunately, conditional compilation as in the following code can't 
work with inout:

void foo(inout(char)[] s)
{
     // ... doesn't mutate s ...

     // NOT A SOLUTION
     static if (is (typeof(s[0]) == immutable)) {
         writeln("immutable");

     } else {
         writeln("mutable");
     }
}

This is not surprising: Since inout is not a template the compiler will 
generate the same code for mutable, const, and immutable. So, in order 
to support all, such parameters cannot be mutated in the function body.

Another way of retaining the mutability information is using templates. 
Since this time the actual "type" would be erased, we can (and should) 
use a template constraint to accept only strings:

void foo(T)(T[] s)
     if (is(Unqual!T == char))
{
     // ... doesn't mutate s ...
}

(Note: Of course the constraint could also be isSomeChar!T, but I wanted 
to continue with the same example that uses 'char'.)

This is great because now we can pass the parameter to to!string 
unconditionally and don't pay anything if the original is already an 
immutable string. Here is a program that demonstrates that nothing is 
being copied for originally immutable strings:

import std.stdio;
import std.traits;
import std.conv;

void foo(T)(T[] s_param)
     if (is(Unqual!T == char))
{
     writeln('\n', s_param);
     writeln("in foo: ", s_param.ptr);

     auto s = to!(string)(s_param);
     bar(s);
}

void bar(string s)
{
     writeln("in bar: ", s.ptr);
}

void main()
{
     char[] m = "originally mutable".dup;
     foo(m);

     foo("originally immutable");
}

Here is the output on my system:

originally mutable
in foo: 2B70E4DE7F80
in bar: 2B70E4DE7F60  // <-- copied

originally immutable
in foo: 47D820
in bar: 47D820        // <-- not copied


CONCLUSION:

* For parameters of reference types that are not modified in the 
function, const is a better choice than immutable because const can take 
both mutable and immutable.

(I still include this among the guidelines under the "How to use" 
section here:

   http://ddili.org/ders/d.en/const_and_immutable.html
)

* The choice above complicates matters when the parameter needs to be 
forwarded to a function that takes as immutable, because 'const' erases 
the mutability information of the actual variable.

* The solution is to make the function a template so that the actual 
type is retained. This solution prevents unnecessary copies when the 
actual variable is already immutable.


QUESTIONS:

What do you think about all of this? Can you see better idioms? Should 
we simply ignore this issue and stick with immutable anyway, especially 
for strings since they are everywhere? Should the original foo() take 
string and have the callers make a copy if the original variable is mutable?

Thank you,
Ali

P.S. I had opened a similar thread about the return types of functions:

   http://forum.dlang.org/thread/itr5o1$poi$1@digitalmars.com

Since then, I have learned that pure functions can simply return by 
mutable because the return values of pure functions can implicitly be 
converted to immutable:

char[] foo() pure     // <-- returns mutable
{
     char[] result;
     return result;
}

void main()
{
     char[] m = foo();  // <-- works
     string s = foo();  // <-- works
}


More information about the Digitalmars-d-learn mailing list