Skip to content

Commit 85c6e3a

Browse files
authored
Add defaultInit to phobos.sys.traits. (#10842)
This is a new symbol and has no equivalent in std.traits. The idea here is that in most circumstances, generic code should not use the init value of a type - either because the init value bypasses default initialization not working, or because the init value is not actually the default initialized value for the type (which happens with non-static nested structs). Some folks use T() to bypass that problem, because it will not compile if default initialization doesn't work, and it will properly initialize the context pointer of non-static nested structs (whereas T.init will leave the context pointers null). However, this does not work in generic code, because it's legal to declare static opCall on structs, and that will hijack T() (and isn't even guaranteed to return a T, let alone the default-initialized value of T). So, defaultInit provides a way to get the default-initialized value of a type within generic code while not compiling when the type cannot actually be default-initialized.
1 parent ae07a90 commit 85c6e3a

File tree

1 file changed

+206
-1
lines changed

1 file changed

+206
-1
lines changed

phobos/sys/traits.d

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
$(LREF SharedOf)
108108
))
109109
$(TR $(TD Misc) $(TD
110+
$(LREF defaultInit)
110111
$(LREF EnumMembers)
111112
$(LREF lvalueOf)
112113
$(LREF rvalueOf)
@@ -1687,6 +1688,210 @@ if (__traits(isTemplate, Template))
16871688
static assert(!isBar!(likeFoo!int));
16881689
}
16891690

1691+
/++
1692+
Evaluates to the default-initialized value of the given type.
1693+
1694+
defaultInit should be used in generic code in contexts where the
1695+
default-initialized value of a type is needed rather than that type's
1696+
$(D init) value.
1697+
1698+
For most types, the default-initialized value of a type $(I is) its
1699+
$(D init) value - i.e. for some type, $(D T), it would normally be
1700+
$(D T.init). However, there are some corner cases in the language where a
1701+
type's $(D init) value is not its default-initialized value. In particular,
1702+
1703+
1. If a type is a non-$(K_STATIC) nested struct, it has a context pointer
1704+
which refers to the scope in which it's declared. So, its
1705+
default-initialized value is its $(D init) value $(I plus) the value for
1706+
its context pointer. However, if such a nested struct is explicitly
1707+
initialized with just its $(D init) value instead of being
1708+
default-initialized or being constructed via a constructor, then its
1709+
context pointer is $(K_NULL), potentially leading to segfaults when the
1710+
object is used.
1711+
2. If a type is a struct for which default initialization has been disabled
1712+
using $(D @disable this();), then while its $(D init) value is still used
1713+
as the value of the object at the start of a constructor call (as is the
1714+
case with any struct), the struct cannot be default-initialized and must
1715+
instead be explicitly constructed. So, instead of $(D T.init) being the
1716+
default-initialized value, it's just the struct's initial state before
1717+
the constructor constructs the object, and the struct does not actually
1718+
have a default-initialized value.
1719+
1720+
In the case of #2, there is no default initialization for the struct,
1721+
whereas in the case of #1, there $(I is) default initialization for the
1722+
struct but only within the scope where the struct is declared. Outside of
1723+
that scope, the compiler does not have access to that scope and therefore
1724+
cannot iniitialize the context pointer with a value, so a compilation error
1725+
results. And so, either case can make it so that a struct cannot be
1726+
default-initialized.
1727+
1728+
In both cases, an instance of the struct can still be explicitly
1729+
initialized with its $(D init) value, but that will usually lead to
1730+
incorrect code, because either the object's context pointer will be
1731+
$(K_NULL), or it's a type which was designed with the idea that it would
1732+
only ever be explicitly constructed. So, while sometimes it's still
1733+
appropriate to explicitly use the $(D init) value (e.g. by default,
1734+
$(REF1 destroy, object) will set the object to its $(D init) value after
1735+
destroying it so that it's in a valid state to have its destructor called
1736+
afterwards in cases where the object is still going to be destroyed when it
1737+
leaves scope), in general, generic code which needs to explicitly
1738+
default-initialize a variable shouldn't use the type's $(D init) value.
1739+
1740+
For a type, $(D T), which is a struct which does not declare a $(K_STATIC)
1741+
$(D opCall), $(D T()) can be used instead of $(D T.init) to get the type's
1742+
default-initialized value, and unlike $(D T.init), it will fail to compile
1743+
if the object cannot actually be default-initialized (be it because it has
1744+
disabled default initialization, or because it's a nested struct outside of
1745+
the scope where that struct was declared). So, unlike $(D T.init), it
1746+
won't compile in cases where default initialization does not work, and thus
1747+
it can't accidentally be used to initialize a struct which cannot be
1748+
default-intiialized. Also, for nested structs, $(D T()) will initialize the
1749+
context pointer, unlike $(D T.init). So, using $(D T()) normally gives the
1750+
actual default-initialized value for the type or fails to compile if the
1751+
type cannot be default-initialized.
1752+
1753+
However, unfortunately, using $(D T()) does not work in generic code,
1754+
because it is legal for a struct to declare a $(K_STATIC) $(D opCall) which
1755+
takes no arguments, overriding the normal behavior of $(D T()) and making
1756+
it so that it's no longer the default-initialized value. Instead, it's
1757+
whatever $(K_STATIC) $(D opCall) returns, and $(K_STATIC) $(D opCall) can
1758+
return any type, not just $(D T), because even though it looks like a
1759+
constructor call, it's not actually a constructor call, and it can return
1760+
whatever the programmer felt like - including $(K_VOID). This means that
1761+
the only way to consistently get a default-initialized value for a type in
1762+
generic code is to actually declare a variable and not give it a value.
1763+
1764+
So, in order to work around that, defaultInit does that for you.
1765+
$(D defaultInit!Foo) evaluates to the default-initialized value of $(D Foo),
1766+
but unlike $(D Foo.init), it won't compile when $(D Foo) cannot be
1767+
default-initialized. And it won't get hijacked by $(K_STATIC) $(D opCall).
1768+
1769+
The downside to using such a helper template is that it will not work with
1770+
a nested struct even within the scope where that nested struct is declared
1771+
(since defaultInit is declared outside of that scope). So, code which needs
1772+
to get a default-initialized instance of a nested struct within the scope
1773+
where it's declared will either have to simply declare a variable and not
1774+
initialize it or use $(D T()) to explicitly get the default-initialized
1775+
value. And since this is within the code where the type is declared, it's
1776+
fully within the programmer's control to not declare a $(K_STATIC)
1777+
$(D opCall) for it. So, it shouldn't be a problem in practice.
1778+
+/
1779+
template defaultInit(T)
1780+
if (is(typeof({T t;})))
1781+
{
1782+
// At present, simply using T.init should work, since all of the cases where
1783+
// it wouldn't won't get past the template constraint. However, it's
1784+
// possible that that will change at some point in the future, and this
1785+
// approach is guaranteed to give whatever the default-initialized value is
1786+
// regardless of whether it's T.init.
1787+
enum defaultInit = (){ T retval; return retval; }();
1788+
}
1789+
1790+
///
1791+
@safe unittest
1792+
{
1793+
static assert(defaultInit!int == 0);
1794+
static assert(defaultInit!bool == false);
1795+
static assert(defaultInit!(int*) is null);
1796+
static assert(defaultInit!(int[]) is null);
1797+
static assert(defaultInit!string is null);
1798+
static assert(defaultInit!(int[2]) == [0, 0]);
1799+
1800+
static struct S
1801+
{
1802+
int i = 42;
1803+
}
1804+
static assert(defaultInit!S == S(42));
1805+
1806+
static assert(defaultInit!Object is null);
1807+
1808+
interface I
1809+
{
1810+
bool foo();
1811+
}
1812+
static assert(defaultInit!I is null);
1813+
1814+
static struct NoDefaultInit
1815+
{
1816+
int i;
1817+
@disable this();
1818+
}
1819+
1820+
// It's not legal to default-initialize NoDefaultInit, because it has
1821+
// disabled default initialization.
1822+
static assert(!__traits(compiles, defaultInit!NoDefaultInit));
1823+
1824+
int var = 2;
1825+
struct Nested
1826+
{
1827+
int i = 40;
1828+
1829+
int foo()
1830+
{
1831+
return i + var;
1832+
}
1833+
}
1834+
1835+
// defaultInit doesn't have access to this scope and thus cannot
1836+
// initialize the nested struct.
1837+
static assert(!__traits(compiles, defaultInit!Nested));
1838+
1839+
// However, because Nested has no static opCall (and we know it doesn't
1840+
// because we're doing this in the same scope where Nested was declared),
1841+
// Nested() can be used to get the default-initialized value.
1842+
static assert(Nested() == Nested(40));
1843+
1844+
Nested nested;
1845+
assert(Nested() == nested);
1846+
1847+
// Both have properly initialized context pointers,
1848+
// whereas Nested.init does not.
1849+
assert(Nested().foo() == nested.foo());
1850+
1851+
// defaultInit does not get hijacked by static opCall.
1852+
static struct HasOpCall
1853+
{
1854+
int i;
1855+
1856+
static opCall()
1857+
{
1858+
return HasOpCall(42);
1859+
}
1860+
1861+
static opCall(int i)
1862+
{
1863+
HasOpCall retval;
1864+
retval.i = i;
1865+
return retval;
1866+
}
1867+
}
1868+
1869+
static assert(defaultInit!HasOpCall == HasOpCall(0));
1870+
static assert(HasOpCall() == HasOpCall(42));
1871+
}
1872+
1873+
@safe unittest
1874+
{
1875+
static struct NoCopy
1876+
{
1877+
int i = 17;
1878+
@disable this(this);
1879+
}
1880+
static assert(defaultInit!NoCopy == NoCopy(17));
1881+
1882+
string function() funcPtr;
1883+
static assert(defaultInit!(SymbolType!funcPtr) is null);
1884+
1885+
string delegate() del;
1886+
static assert(defaultInit!(SymbolType!del) is null);
1887+
1888+
int function() @property propFuncPtr;
1889+
static assert(defaultInit!(SymbolType!propFuncPtr) is null);
1890+
1891+
int delegate() @property propDel;
1892+
static assert(defaultInit!(SymbolType!propDel) is null);
1893+
}
1894+
16901895
/++
16911896
Evaluates to an $(D AliasSeq) containing the members of an enum type.
16921897
@@ -6697,7 +6902,7 @@ if (!is(sym))
66976902
$(DDSUBLINK spec/traits, getOverloads, $(D __traits(getOverloads, ...))
66986903
must be used.
66996904
6700-
In general, $(getOverloads) should be used when using SymbolType, since
6905+
In general, $(D getOverloads) should be used when using SymbolType, since
67016906
there's no guarantee that the first one is the correct one (and often, code
67026907
will need to check all of the overloads), whereas with PropertyType, it
67036908
doesn't usually make sense to get specific overloads, because there can

0 commit comments

Comments
 (0)