C++ Error C2228 (left of '.val' must have class/struct/union) in unusual circumstances
Categories:
Demystifying C++ Error C2228: 'left of .val must have class/struct/union' in Modern C++

Explore the nuances of C++ Error C2228, particularly when it arises in unexpected scenarios involving std::any
, decltype
, and type deduction. Learn to diagnose and resolve this common compiler error.
The C++ compiler error C2228, "left of '.val' must have class/struct/union," is a common diagnostic that indicates you're trying to access a member (.val
) on something that the compiler doesn't recognize as a class, struct, or union type. While often straightforward to fix (e.g., dereferencing a pointer), this error can become particularly perplexing when dealing with modern C++ features like std::any
, decltype
, and complex type deduction. This article delves into these less obvious scenarios, providing insights and solutions to help you navigate such challenges.
Understanding the Core of C2228
At its heart, C2228 means you're using the dot operator (.
) on an expression whose type is not a class, struct, or union. The dot operator is exclusively for direct member access of objects. If you have a pointer to an object, you must use the arrow operator (->
) or dereference the pointer first ((*ptr).member
).
struct MyStruct {
int val;
};
int main() {
MyStruct obj;
obj.val = 10; // OK: obj is a struct
MyStruct* ptr = &obj;
// ptr.val = 20; // ERROR C2228: ptr is a pointer
ptr->val = 20; // OK: ptr is a pointer, use ->
(*ptr).val = 30; // OK: dereference first
int x = 5;
// x.val = 1; // ERROR C2228: x is an int, not a class/struct/union
return 0;
}
Basic examples demonstrating correct and incorrect usage leading to C2228.
C2228 with std::any
and Type Extraction
std::any
is a C++17 feature that allows storing values of any type. When you retrieve a value from std::any
, you must explicitly cast it back to its original type using std::any_cast
. A common mistake is to forget this cast or to cast to an incorrect type, leading to C2228 if you then try to access a member.
flowchart TD A[std::any object] --> B{Retrieve value}; B --> C{Is std::any_cast used?}; C -- No --> D[Error C2228: Attempt to access member on std::any]; C -- Yes --> E{Is cast type correct?}; E -- No --> F[std::bad_any_cast exception]; E -- Yes --> G[Access member successfully];
Flowchart illustrating the process of retrieving values from std::any
and potential C2228 pitfalls.
#include <any>
#include <iostream>
#include <string>
struct Data {
int id;
std::string name;
};
int main() {
std::any myAny = Data{1, "Example"};
// Incorrect: myAny is std::any, not Data
// std::cout << myAny.id << std::endl; // ERROR C2228
// Correct: Cast to Data first
try {
Data d = std::any_cast<Data>(myAny);
std::cout << "ID: " << d.id << ", Name: " << d.name << std::endl;
} catch (const std::bad_any_cast& e) {
std::cerr << "Bad any_cast: " << e.what() << std::endl;
}
// Incorrect cast type
try {
int i = std::any_cast<int>(myAny);
// This line won't be reached if bad_any_cast occurs
// std::cout << i.id << std::endl; // Would be C2228 if it compiled
} catch (const std::bad_any_cast& e) {
std::cerr << "Caught expected bad_any_cast: " << e.what() << std::endl;
}
return 0;
}
Demonstrating C2228 with std::any
and the correct usage of std::any_cast
.
The decltype
and auto
Trap
When decltype
or auto
are used in conjunction with expressions that return references, it's easy to inadvertently end up with a reference type. If you then try to use the dot operator on this reference type, the compiler might issue C2228, especially if the reference is to a primitive type or if the context expects a non-reference type.
decltype(expr)
yields the exact type of expr
, including lvalue/rvalue references. If expr
is an lvalue of type T
, decltype(expr)
is T&
. If expr
is an rvalue of type T
, decltype(expr)
is T
.#include <iostream>
#include <string>
struct Point {
int x, y;
};
Point getPoint() {
return {10, 20};
}
int main() {
Point p_obj = getPoint();
// Case 1: decltype on an lvalue
decltype(p_obj) p_ref = p_obj; // p_ref is Point&
std::cout << p_ref.x << std::endl; // OK: p_ref is a reference to a struct
// Case 2: decltype on a function call returning a temporary (rvalue)
decltype(getPoint()) p_val = getPoint(); // p_val is Point (not Point&)
std::cout << p_val.x << std::endl; // OK: p_val is a struct
// Case 3: decltype on a member access (lvalue)
decltype(p_obj.x) x_ref = p_obj.x; // x_ref is int&
// x_ref.val = 5; // ERROR C2228: x_ref is an int&, not a class/struct/union
std::cout << x_ref << std::endl; // OK: x_ref is an int&
// Case 4: auto with reference
auto& x_auto_ref = p_obj.x; // x_auto_ref is int&
// x_auto_ref.val = 5; // ERROR C2228: x_auto_ref is an int&, not a class/struct/union
std::cout << x_auto_ref << std::endl; // OK
return 0;
}
Illustrating C2228 with decltype
and auto&
when dealing with primitive types.
decltype
with expressions that are not simple variable names, as the resulting type can be a reference. For example, decltype((var))
where var
is a variable of type T
will yield T&
, even if var
itself is not a reference.Debugging Strategies for C2228
When faced with C2228 in complex scenarios, systematic debugging is key. Here are some strategies:
1. Inspect the Type
Use static_assert
with std::is_same
or typeid().name()
(though typeid
can be misleading with references) to determine the exact type of the expression to the left of the dot operator. For example: static_assert(std::is_same_v<decltype(myVar), MyStruct>, "Type mismatch!");
or std::cout << typeid(myVar).name() << std::endl;
.
2. Simplify the Expression
Break down complex expressions into smaller, intermediate variables. This can help isolate which part of the expression is yielding an unexpected type.
3. Check for Pointers
If the type is a pointer (e.g., MyStruct*
), remember to use ->
instead of .
.
4. Verify std::any_cast
If std::any
is involved, ensure you are using std::any_cast
with the correct target type before attempting member access.
5. Review decltype
and auto
Usage
If decltype
or auto
are involved, carefully consider whether the deduced type is a reference (T&
) or a value (T
), especially when the expression is an lvalue.