Both languages have both destructuring (var (x, y) = vec;) and attributes ([MyAttribute]). C++ differs from C# in several ways, so today we’ll take a look at those differences and learn how to make use of these language features.

Table of Contents

Structured Bindings

“Destructuring” in C# is the process by which we extract the fields out of a struct or class and into individual variables. It looks like this:

// C#
 
// A type we want to destructure
struct Vector2
{
    public float X;
    public float Y;
 
    // Create a Deconstruct method that takes an 'out' param for each variable
    // to deconstruct into and returns void
    public void Deconstruct(out float x, out float y)
    {
        x = X;
        y = Y;
    }
}
 
// Instantiate the deconstructable type
var vec = new Vector2{X=2, Y=4};
 
// Deconstruct. Implicitly calls vec.Deconstruct(out x, out y).
// x is a copy of vec.X and y is a copy of vec.Y
var (x, y) = vec;
 
DebugLog(x, y); // 2, 4

In C++ terminology, we don’t “deconstruct” but rather “create structured bindings.” Here’s the equivalent code:

// A type we want to crate structured bindings for
struct Vector2
{
    float X;
    float Y;
};
 
// Instantiate that type
Vector2 vec{2, 4};
 
// Create structured bindings. x is a copy of vec.X and y is a copy of vec.Y.
auto [x, y] = vec;
 
DebugLog(x, y); // 2, 4

So far it’s essentially the same in the two language except for two changes. First, C++ uses square brackets ([x, y]) instead of parentheses ((x, y)). Second, C++ doesn’t require us to write a Deconstruct function. Instead, the compiler simply uses the declaration order of the fields of Vector2 so that x lines up with X and y with Y. This mirrors initialization where Vector2{2, 4} initializes the data members in declaration order: X then Y.

This can be customized to by “tuple-like” types, but it takes more work than in C#:

struct Vector2
{
    float X;
    float Y;
 
    // Get a data member of a const vector
    template<std::size_t Index>
    const float& get() const
    {
        // Assert the only two valid indices
        static_assert(Index == 0 || Index == 1);
 
        // Return the right one based on the index
        if constexpr(Index == 0)
        {
            return X;
        }
        return Y;
    }
 
    // Get a data member of a non-const vector
    template <std::size_t Index>
    float& get()
    {
        // Cast to const so we can call the const overload of this function
        // to avoid code duplication
        const Vector2& constThis = const_cast<const Vector2&>(*this);
 
        // Call the const overload of this function
        // Returns a const reference to the data member
        const float& constComponent = constThis.get<Index>();
 
        // Cast the data member to non-const
        // This is safe since we know the vector is non-const
        float& nonConstComponent = const_cast<float&>(constComponent);
 
        // Return the non-const data member reference
        return nonConstComponent;
    }
};
 
// Specialize the tuple_size class template to derive from integral_constant
// Pass 2 since Vector2 always has 2 components
template<>
struct std::tuple_size<Vector2> : std::integral_constant<std::size_t, 2>
{
};
 
// Specialize the tuple_element struct to indicate that index 0 of Vector2 has
// the type 'float'
template<>
struct std::tuple_element<0, Vector2>
{
    // Create a member named 'type' that is an alias for 'float'
    using type = float;
};
 
// Same for index 1
template<>
struct std::tuple_element<1, Vector2>
{
    using type = float;
};
 
// Usage is the same
Vector2 vec{2, 4};
auto [x, y] = vec;
DebugLog(x, y);

The result of the above code is that Vector2 is now a “tuple-like” type, usable with structured bindings and several generic algorithms of the C++ Standard Library.

The final kind of object we can create structured bindings for is an array. This isn’t allowed by default in C#:

// Create an array of two int elements
int arr[] = { 2, 4 };
 
// Create structured bindings. x is a copy of arr[0] and y is a copy of arr[1].
auto [x, y] = arr;
 
DebugLog(x, y); // 2, 4

It’s important to note that the structured bindings we’ve been creating have to be automatically-typed with auto. Even if we know the type, we can’t use it:

int arr[] = { 2, 4 };
 
// Compiler error: must use the 'auto' type here
int [x, y] = arr;

What we are allowed to do is create structured binding references with auto&:

int arr[] = { 2, 4 };
 
// Create copies of arr[0] and arr[1]
auto [xc, yc] = arr;
 
// Create references to arr[0] and arr[1]
auto& [xr, yr] = arr;
 
// Modify the elements of the array
arr[0] = 20;
arr[1] = 40;
 
DebugLog(xc, yc); // 2, 4
DebugLog(xr, yr); // 20, 40

The same is also true for rvalue references via auto&&:

Vector2 Add(Vector2 a, Vector2 b)
{
    return Vector2{a.X+b.X, a.Y+b.Y};
}
 
// Compiler error: return value of Add isn't an lvalue so can't take lvalue ref
auto& [x, y] = Add(Vector2{2, 4}, Vector2{6, 8});
 
// OK
auto&& [x, y] = Add(Vector2{2, 4}, Vector2{6, 8});
DebugLog(x, y); // 8, 12

Both forms of references as well as copies may also be const:

Vector2 vec{2, 4};
 
// Constant copy
const auto [xc, yc] = vec;
 
// Constant lvalue reference
const auto& [xr, yr] = vec;
 
// Constant rvalue reference
const auto&& [xrr, yrr] = Add(Vector2{2, 4}, Vector2{6, 8});

We can also use other forms of initialization when creating structured bindings. So far we’ve copy-initialized our variables with = but we can also direct-initialize them with {} and ():

Vector2 vec{2, 4};
 
// Copy-initialize
auto [x1, y1] = vec;
 
// Direct-initialize with curly braces
auto [x2, y2]{vec};
 
// Direct-initialize with parentheses
auto [x3, y3](vec);

Finally, C# supports ignoring some deconstructed variables with “discards” in the form of _. This isn’t supported in C++, so we’ll need to explicitly name and ignore them to avoid a compiler warning:

Vector2 vec{2, 4};
 
auto [x, y] = vec;
static_cast<void>(x); // One way of explicitly ignoring x
(void)x; // Another way of ignoring x
 
// Use only y
DebugLog(y); // 4
Attributes

C# attributes associate some metadata with an entity like a class, a method, or a parameter. This metadata can be used in one of two ways. First, the compiler can query attributes like [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] to modify how compilation works. Second, our C# code can query attributes at runtime via reflection to make use of it in various custom ways.

As C++ doesn’t support reflection, it’s usage of attributes doesn’t cover the runtime use cases. It does, however, cover the compile-time use cases. Because this functionality is implemented by the compiler, not us, we can’t create custom attributes. Instead, we use two built-in sets of attributes. First, there are several attributes defined by the C++ language:

Attribute Version Meaning
[[noreturn]] C++11 This function will never return
[[carries_dependency]] C++11 Unnecessary memory fence instructions for this can be removed in some situations
[[deprecated]] C++14 This is deprecated
[[deprecated("why")]] C++14 This is deprecated for a specific reason
[[fallthrough]] C++17 This case in a switch intentionally falls through to the next case
[[nodiscard]] C++17 A compiler warning should be generated if this is ignored
[[nodiscard("msg")]] C++20 A compiler warning with a particular message should be generated if this is ignored
[[maybe_unused]] C++17 No compiler warning should be generated for not using this
[[likely]] C++20 This branch is likely to be taken
[[unlikely]] C++20 This branch is unlikely to be taken
[[no_unique_address]] C++20 This non-static data member doesn’t need to have a unique memory address

There are two aspects of these to take notice of. First, and trivially, C++ attributes use two square brackets ([[X]]) instead of one in C# ([X]). Second, all of the attributes are for one of two purposes: controlling compiler warnings and optimizing generated code.

The second set of attributes is compiler-specific and not part of the C++ standard. Clang, for example, has a ton of them for various purposes. If these are ever specified and the compiler doesn’t support them, the’re simply ignored.

Now let’s look at some code that uses them:

class File
{
    FILE* handle = nullptr;
 
public:
 
    ~File()
    {
        if (handle)
        {
            ::fclose(handle);
        }
    }
 
    // Generate a compiler warning if the return value is ignored
    [[nodiscard]] bool Close()
    {
        if (!handle)
        {
            return true;
        }
        return ::fclose(handle) == 0;
    }
 
    // Generate a compiler warning if the return value is ignored
    [[nodiscard]] bool Open(const char* path, const char* mode)
    {
        if (!handle)
        {
            // No compiler warning because return value is used
            if (!Close())
            {
                return false;
            }
        }
        handle = ::fopen(path, mode);
        return handle != nullptr;
    }
};
 
File file{};
 
// Compiler warning: return value ignored
file.Open("/path/to/file", "r");
 
// Compiler warning: unused variable
bool success = file.Open("/path/to/file", "r");
 
// No compiler warning: suppress the unused variable
[[maybe_unused]] bool success = file.Open("/path/to/file", "r");
 
// No compiler warning because return value is used
if (!file.Open("/path/to/file", "r"))
{
    DebugLog("Failed to open file");
}

To use more than one attribute at a time, add commas like when declaring multiple variables at a time:

// Both [[deprecated("why")]] and [[nodiscard]]
[[deprecated("Wasn't very good. Use Hash2() instead."), nodiscard]]
uint32_t Hash(const char* bytes, std::size_t size)
{
    uint32_t hash = 0;
    for (std::size_t i = 0; i < size; ++i)
    {
        hash += bytes[i];
    }
    return hash;
}

For attributes in namespaces, such as provided by compilers, we use the familiar scope resolution operator (::) to refer to them:

// Use the "nodebug" attribute in the "gnu" namespace
// Do not generate debugging information for this function
[[gnu::nodebug]]
float Madd(float a, float b, float c)
{
    return a * b + c;
}

When using multiple attributes in namespaces, there’s a shortcut since C++17 that avoids duplicating the namespace name:

// Both [[gnu::returns_nonnull]] and [[gnu::nodebug]]
[[using gnu: returns_nonnull, nodebug]]
void* Allocate(std::size_t size)
{
    if (size < 4096)
    {
        return SmallAlloc(size);
    }
    return BigAlloc(size);
}

Attributes can appear in a great many places in C++: variables, functions, names, blocks, return values, and so forth. If adding the attribute makes logical sense, it’s probably allowed.

Conclusion

C++ structured bindings are a different take on C#’s destructuring. We don’t need to write any code at all to create structured bindings for structs and arrays. For tuple-like types, we have to write quite a bit more than a Destructure method in C#. Thankfully, that’s rarely needed due to the presence of the std::tuple class template in the Standard Library which is obviously tuple-like and therefore supports destructuring.

C++ attributes are one of the rare areas of the language that’s actually less powerful than its C# counterpart. It fulfills compile-time purposes such as by controlling warnings and optimization, but it doesn’t support any run-time use cases due to the lack of reflection in the language. Third-party libraries (example) are required to add on run-time reflection if needed, but they’re not integrated into the core language. This may change in C++23 or another future version as there has been much work on integrating compile-time reflection into the language.