Thread overview
Temporaries / struct RAII patterns
Oct 16, 2023
Witold
Oct 16, 2023
Witold
Oct 16, 2023
Witold
October 16, 2023

Not sure how to name the problem, but here it is.

I want to create a struct instance on a stack using a helper function, manipulate it Fluent-like style, then destructor be invoked, that will do something with its state.

I do not want to use Unique, because that will allocate actual data on heap. And unique will have pointer to it. This is because one can do release and move on Unique. I do not need (or want) release or move.

I want something that can be done on a stack, but can be manipulated and passed to functions, and destructor called once I either go out of scope, or it is not used at all, and expression / statement ends.

struct X {
  string message;
  string file;
  int line;
  string[string] kvs;

  ref X F(string fmt, Args...)(const Args args) {
    return this;
    ...
  }
  ref X KV(string k, string v) {
    kvs[k] = v;
    return this;
  }
  ~this() {
     DoSomething(&this, message, kvs);
  }
}

X Foo(const string message, string file = __FILE__, int line = __LINE__) {
  return X(message, file, line)
}

X Foo(string fmt, Args...)(lazy Args args, string file = __FILE__, int line = __LINE__) {
  return X(message, file, line).F!(fmt, Args)(args);
}


void main() {
   Foo("a");
   Foo("b").kv("k1", "v1").kv("k2", "v2");
   Foo!"c %d %s"(1, "ble").kv("k3", "v3");
}

I know I can do this:

void main() {
   {
     scope x = Foo("a");
   }
   {
     scope x = Foo("b");
     x.KV("k1", "v1");
     x.KV("k2", "v2");
   }
}

But this is not going to scale (I will have many of these statements in each file and function), and now requires me to remember to put scope in front of each variable (scope constraint keyword on struct type itself is deprecated), which is error prone.

How do I ensure that X is not copied (i.e. on return from f), or when doing kvs (and similar functions), and that X is allocated on the stack, and is destroyed properly.

I also do want to be able to d the second form (with explicit scope and calls to kv, i.e. when doing kv calls in a loop from some other array or associative array).

I cannot use ref return on the Foo, because it the thing I am returning either is not an lvalue, or any lvalue it could reference would be on stack of Foo, so it will be returning invalid reference after Foo returns.

Cheers.

October 16, 2023

Also I added

       // Disables default construction
       @disable this();
       // Disable copy constructor
       @disable this(ref X);

to struct X,

and that triggers an error, because compiler is trying to call a copy constructor when doing return from Foo.

October 16, 2023

So it looks like to ensure RVO (return value optimization), I need to write it like this:

X Foo(string fmt, Args...)(lazy Args args, string file = __FILE__, int line = __LINE__) {
  auto r = X(message, file, line);
  r.F!(fmt, Args)(args);
  return r;
}

Instead of:

X Foo(string fmt, Args...)(lazy Args args, string file = __FILE__, int line = __LINE__) {
  return X(message, file, line).F!(fmt, Args)(args);
}

Otherwise copy constructor is called. In this case I can emulate "move" semantic using copy constructor:

struct X {
  @disable this();
  this(ref X other) {
    message = other.message;
    file = other.file;
    line = other.line;
    kvs = other.kvs;

    // mark other moved out
    other.message = null;
    other.file = null;
    other.line = -1;
    other.kvs = null;
  }

  // ...
  ~this() {
    // Only do action if it was not moved out by "copy" constructor
    if (message !is null) {
      DoSomething(&this, message, kvs);
    }
  }
}

But it is so so.

I do not plan to expose X type directly, so nobody but the library should be creating it, but user can technically assign it to variables (there are reasons for this), for future manipulation.