Jan 26, 2021

[C++] trivial_abi and borrow ownership

An interesting concept comes up from Rust's borrow ownership.

In C++, the unique_ptr can only be moved. While instead of using heavy-weighted shared_ptr
(i.e 2 words size, heap allocated control block, atomic ref-counting, weak_ptr etc.), the only way of borrow the ownership from unique_ptr is by passing raw pointer from unique_ptr.

Here's the tweet from Prof. John Regehr talking about this:
https://twitter.com/johnregehr/status/1351333748277592065
Follow up from s.o:
https://stackoverflow.com/a/30013891
i.e in C++, the borrow concept can't be made in compile time, but in runtime, which unlike Rust.

Another to mention about is trivial_abi.
AMD64 ABI for C++: https://www.uclibc.org/docs/psABI-x86_64.pdf

While call by value,

Quote from ABI:
If a C++ object has either a non-trivial copy constructor or a non-trivial destructor, it is passed by invisible reference (the object is replaced in the parameter list by a pointer […]).

And in Itanium C++ ABI document, quote:

non-trivial for the purposes of calls
A type is considered non-trivial for the purposes of calls if: it has a non-trivial copy constructor, move constructor, or destructor, or all of its copy and move constructors are deleted.
This definition, as applied to class types, is intended to be the complement of the definition in [class.temporary]p3 of types for which an extra temporary is allowed when passing or returning a type. A type which is trivial for the purposes of the ABI will be passed and returned according to the rules of the base C ABI, e.g. in registers; often this has the effect of performing a trivial copy of the type.

That is call by value with non-trivial type introduce double indirection during the call while the type could potentially not fit into the register for callee.  By double indirection meaning the argument is reference to the temporary r-value created on caller's stack.
i.e potential virtual pointer to v-table increases the size of type.


Reference: https://www.raywenderlich.com/615-assembly-register-calling-convention-tutorial

Assembly code can be found in godbolt:
https://godbolt.org/z/s1TPea6sx


#include <cstdio>

#define TRIVIAL_ABI __attribute__((trivial_abi))

template <class T> T incr(T obj) {
  obj.value += 1;
  puts("before exit incr func");
  return obj;
}

struct Up1 {
  int value;
  Up1() = default;
  Up1(const Up1& u) : value(u.value) { puts("Up1 copy constructor"); }
  ~Up1() { printf("detroyed Up1 value: %d\n", value); }
};

struct TRIVIAL_ABI Up2 {
  int value;
  Up2() = default;
  Up2(const Up1& u) : value(u.value) { puts("Up2 copy constructor"); }
  ~Up2() { printf("detroyed Up2 value: %d\n", value); }
};

template Up1 incr(Up1);
template Up2 incr(Up2);

auto main() -> int {
  auto u1 = Up1{};
  puts("before call incr func for u1");
  incr(u1);

  printf("\n\n");

  auto u2 = Up2{};
  puts("before call incr func for u2");
  incr(u2);
}

Output:
before call incr func for u1
Up1 copy constructor; temporary object creatd;
before exit incr func
Up1 copy constructor; temporary object creatd;
detroyed Up1 value: 1
detroyed Up1 value: 1


before call incr func for u2
// No temporary object created, all in register.
before exit incr func
detroyed Up2 value: 1
detroyed Up2 value: 1
detroyed Up2 value: 0
detroyed Up1 value: 0

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.