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