1. 12

  2. 4

    Really nice article which digs down into the details of how monomorphization and trait objects actually end up being implemented in compiled code. For people who want that level of detail, this is a solid explanation of the nitty-gritty and trade-offs.

    It’s worth noting that the rules around object safety are a bit more complex than is described here. It’s orthogonal to the point of the article, so no real fault to the other for not covering it.

    The key constraint of object safety is about ensuring the trait object is constructable, meaning there is a sensible way for the trait object itself to implement the trait and its methods (with the generated method implementations performing dynamic dispatch to the proper implementation of the contained concrete type at runtime).

    Constructability constraints include:

    • avoiding passing an unsized type to a function (hence the requirement that trait methods can’t take self by value, or else must constrain the function to only be defined when Self: Sized),
    • avoiding the use of generic parameters (because the calls are resolved at runtime, at compile time it’s not clear what the generic parameters ought to be filled with)
    • avoiding the use of the concrete type in a manner which is not implementable with the limited runtime type information preserved for trait objects.

    The second constraint means that accessing the associated types of a type’s impl of the trait in question, or any supertrait of it, is fine, but accessing the Self type in other ways is not.

    Object safety is one of those things that tends to trip people up, and at times there are suggestions to eliminate the object safety rules, but truly they’re there because there’s no clear to function without them. What’s the point of a trait object which can’t implement the trait it’s based on?

    If you find yourself stuck with a trait that isn’t object safe, you do have some options (disclosure: I wrote this article).

    1. 1

      The biggest issue I’ve run into is the tradeoff between static and dynamic dispatch writing at the library level. While some libraries have a very clear user where the which tradeoff to choose is more obvious, and/or small enough to write two variants. Other libraries are less obvious and half of the consumers require performance, while the others require small binary size.

      On the other side of the coin, it can be frustrating to find a find a library that meets all requirements except for simply picking the tradeoff that isn’t as conducive to your requirements (i.e. they picked dynamic dispatch, but you’d prefer static, or vice a versa).

      For the binary size, previously the case was that Rust (or perhaps LLVM?) could do a better job with pruning of unused generic functions. Last I checked, Rust wasn’t using the LLVM passes that more aggressively removed generic functions for unused types. Although, I don’t remember the details as to why, or if more progress has been made in this area lately.