On Sunday, 30 April 2017 at 21:43:26 UTC, Andrei Alexandrescu
wrote:
On 04/27/2017 07:35 PM, Stanislav Blinov wrote:
IAllocator is too high level an interface, it doesn't carry any
information as to what type of memory it can allocate (so we
can only
assume unshared), and does or does it not use GC (so we can
only assume
GC).
Initially all fresh memory is unshared. Whether or not the user
subsequently shares it is of no consequence to the allocator.
Why would we need any ISharedAllocator then? If we're to force
users to rely on *documentation* whether or not they can cast
allocated memory to shared, there is no point in static checks,
they'll only get in the way of user's attempts to try and stitch
together the pieces. They're on their own anyway at that point.
If we are to devise types with allocators as members instead
of type
arguments, we need the additional information. Better to catch
invalid
assignment at compile time than to chase down how a stack
allocator from
one thread ended up in another.
A pass through the root allocators (Mallocator, GCAllocator
etc) figuring out what attributes could be meaningfully
attached would be welcome. The rest would rely on inference.
If we're type-erasing allocators, we have to have the ability to
statically specify what traits we're interested in (i.e. *don't*
want to erase). Otherwise, inference will not be of any help.
Consider an example, inspired by the discussion of Atila's
automem library:
import std.experimental.allocator;
import std.algorithm.mutation : move;
// Allocator is not a type parameter
struct SmartPtr(T)
{
// we can infer all we need form A...
this(A, Args...)(A alloc, auto ref Args args)
if (isAllocatorImpl!A)
{
// ...but we immediately throw the inference away:
allocator_ = alloc;
block_ = cast(Block[])allocator_.allocate(Block.sizeof);
// SmartPtr logic snipped...
}
~this()
{
// ...SmartPtr logic snipped
allocator_.deallocate(block_);
}
private:
struct Block
{
size_t refCount;
void[T.sizeof] payload;
}
Block[] block_;
IAllocator allocator_;
}
struct Data {
~this() @nogc { /*...*/ }
// the rest of implementation is @nogc as well
}
struct MyType
{
// won't compile: IAllocator's methods aren't @nogc,
// so SmartPtr's dtor isn't either
this(SmartPtr!Data data) @nogc
{
data_ = move(data);
}
private:
SmartPtr!Data data_;
}
Obviously, IAllocator is not the tool for the job here. This
calls for something like this:
enum AllocTraits
{
none = 0x00,
nogc = 0x01,
share = 0x02
}
alias AllocatorInterface(AllocTraits) = // ???
struct SmartPtr(T, AllocTraits traits = AllocTraits.none)
{
this(A, Args...)(A alloc, auto ref Args args)
{
// this should not compile if A isn't compatible
// with `traits`:
allocator_ = alloc;
block_ = allocator_.allocate(T.sizeof);
// SmartPtr logic snipped...
}
~this()
{
// ...SmartPtr logic snipped
allocator_.deallocate(block_);
}
private:
void[] block_;
AllocatorInterface!AllocTraits allocator_;
}
alias DataPtr = SmartPtr!(Data, AllocTraits.nogc);
struct MyType
{
this(DataPtr data) @nogc
{
data_ = move(data);
}
private:
DataPtr data_;
}
That *would* be able to rely on inference. Question is, what is
AllocatorInterface? Should we push such type erasure to the users?