0015 - const-qualified Non-static Member Functions
| Status | Under Consideration |
|---|---|
| Author | |
| Sponsor |
Implementation Status
| DXC | Clang | |
|---|---|---|
const member functions | Prototype implementation | Complete |
Introduction
HLSL does not currently support const non-static member functions for user-defined data
types. This proposal seeks to add support for const non-static member functions, and to
adopt const-correct behaviors across the HLSL library objects.
Motivation
The absence of const non-static member functions causes some challenges since HLSL
injected ASTs do have const and non-const non-static member functions.
Further, since variables can be const-qualified, without the ability to
specify const there are some cases that cannot be worked around without
breaking const-correctness.
DXC has received a number of issues relating to this. Two recent issues are listed below:
- https://github.com/microsoft/DirectXShaderCompiler/issues/4340
- https://github.com/microsoft/DirectXShaderCompiler/issues/4706
In the first issue, a user defined data type’s functions are basically unusable
if the data type is placed in a ConstantBuffer. This results from the
ConstantBuffer access returning const & objects.
The second issue describes a similar problem, where overloaded operators on
instances of const-qualified user-defined types are unusable.
Proposed solution
Following C++, HLSL will enable support for const non-static member functions and
instance operator overloads (henceforth collectively referred to as constant
member functions) to allow execution of member functions on const objects and
preserve const qualifiers.
Updates to HLSL’s built-in data types to observe best practices in const-correctness will follow the introduction of language support. Functions which return mutable lvalue references will become non-constant member functions, and functions which return constant lvalue references or object values will become constant member functions.
Syntactic Changes
C++’s existing syntax for declaring constant instance functions is compatible
with HLSL. Adoption of this syntax does not introduce any syntactic ambiguities
with existing HLSL constructs. Adding the const keyword to the end of the
function declarator before the optional function body will denote a const
member function function. The const keyword applies to the implicit object
argument so it can only be applied to non-static member functions. See the
examples below defining both a constant function and a constant overload of the
call () operator:
struct Pupper {
void Wag() const { /* body omitted */ }
void operator() const { /* body omitted */ }
void Walk() mutable { /* body omitted */ }
mutable int Speed;
};
Semantic Changes
In a const member function, the implicit object parameter (this) becomes a
constant lvalue reference (const &). Code modifying any field of the this
object is ill-formed and will produce a diagnostic. Calls to non-constant member
functions are also ill-formed and will produce a diagnostic.
This change requires modifications to HLSL’s overload resolution rules to account for the const-ness of object parameters. When performing lookup of possible overload candidates, overloaded functions with non-constant implicit object parameters are invalid candidates when the implicit object is constant.
Standard HLSL argument promotion rules will apply for the object parameter, but
they cannot remove the const qualifier and shall not convert from a constant
lvalue to a non-constant rvalue by copying the implicit argument as is valid for
other arguments.
HLSL Data Type Changes
Introducing constant member functions provides an opportunity to revisit the const-correctness patterns of existing HLSL data types. With this change we will perform an audit of existing data types to provide constant and non-constant member functions as appropriate for the data type.
When applied to HLSL intangible types, the const qualifier will apply as if to
the handle, not the data the handle grants access to. For example, a const RWBuffer<T> will still allow writes to the underlying resource, however the
resource variable itself cannot be re-assigned.
Impact on Existing Code
Supporting constant member function overload resolution will break existing code
that calls member functions on cbuffer, tbuffer or global constant variables.
Consider the following valid HLSL:
struct Hat {
int getFeathers() {
return Feathers;
}
int Feathers;
};
cbuffer CB {
Hat H;
};
export int GetFeatherCount() {
return H.getFeathers();
}
This code is valid under HLSL 2021 because HLSL ignores the const-ness of the
implicit object parameter, however any writes to the constant data are dropped.
On introducing constant member functions, this code is ill-formed because these
declarations inside a cbuffer are implicitly constant and will produce a
diagnostic.
Const-correct Resources
Implementing const-correct member functions on built-in HLSL data types should have no disruption to users.
Consider the following code:
void setValue(RWBuffer<int> R, int Val, int Index) {
R[Index] = Val;
}
void setValueConst(const RWBuffer<int> R, int Val, int Index) {
setValue(R, Val, Index);
}
In setValueConst, the const qualifier applies to the instance of the
RWBuffer parameter. A new RWBuffer<T> variable can be created from a const RWBuffer<T> via copy-initialization (standard copy construction), allowing
setValueConst to call setValue. This does not violate const-correctness
since the handle is treated as const while the data it references is not.
NOTE: DXC already generates methods on built-in objects as
constin the hardcoded AST generation code.