Thanks for the feedback @eskimo. You actually helped clarify the point about QoS overrides not being reported via qos_class_self() in a recent other question of mine, which was very helpful in this subsequent investigation. The reason I used qos_class_self() in the example was mostly for convenience, since both the other tools require a bit more overhead to illustrate the same thing (though I have used taskinfo and it reports the same behavior).
I believe the topic here is a bit different than QoS 'overrides', as it's more a question of how a queue's QoS is to be resolved when there is ambiguity between the explicit qos parameter and the QoS value of its target queue. IIUC, this value should essentially be known (or knowable) when the queue is constructed, at least in most cases. In the 'Modernizing Grand Central Dispatch Usage' from WWDC 2017, there is a segment explaining how queue hierarchies are supposed to interact with QoS, and at one point it's stated that:
Another common use case would be to put a label on the mutual exclusion queue to provide a floor of execution so that nothing in this tree can execute below this level, so [utility] in this example.
To me, this, along with the documentation for dispatch_set_target_queue which states:
A dispatch queue inherits the minimum quality-of-service level from its target queue.
suggests that the QoS of the target queue should govern the minimum 'effective' QoS for submitted work items (again, assuming no other QoS rules are in play).
However, upon further research, I did find that there is this additional statement from the docs for dispatch_queue_attr_make_with_qos_class alluding to this issue:
The quality-of-service value you specify using this function takes precedence over the priority level inherited from the dispatch queue’s target queue.
That, combined with what looks like the relevant logic from the open source libdispatch code probably explains why I'm observing this behavior, though it still puzzles me if this is actually intended. I guess fundamentally, I would assume that both creating a queue and immediately setting its target or using the 'atomic' constructor that does both things at once would result in the same end state, at least in regards to the QoS topic here.