How to change const std::shared_ptr after first assignment? - c++

If I define a shared_ptr and a const shared_ptr of the same type, like this:
std::shared_ptr<int> first = std::shared_ptr<int>(new int);
const std::shared_ptr<int> second = std::shared_ptr<int>();
And later try to change the value of the const shared_ptr like this:
second = first;
It cause a compile error (as it should). But even if I try to cast away the const part:
(std::shared_ptr<int>)second = first;
The result of the code above is that second ends up being Empty, while first is untouched (eg ref count is still 1).
How can I change the value of a const shared_ptr after it was originally set? Is this even possible with std's pointer?
Thanks!

It is undefined behavior to modify in any way a variable declared as const outside of its construction or destruction.
const std::shared_ptr<int> second
this is a variable declared as const.
There is no standard compliant way to change what it refers to after construction and before destruction.
That being said, manually calling the destructor and constructing a new shared_ptr in the same spot might be legal, I am uncertain. You definitely cannot refer to said shared_ptr by its original name, and possibly leaving the scope where the original shared_ptr existed is illegal (as the destructor tries to destroy the original object, which the compiler can prove is an empty shared pointer (or a non-empty one) based on how the const object was constructed).
This is a bad idea even if you could make an argument the standard permits it.
const objects cannot be changed.
...
Your cast to a shared_ptr<int> simply creates a temporary copy. It is then assigned to, and the temporary copy is changed. Then the temporary copy is discarded. The const shared_ptr<int> not being modified is expected behavior. The legality of assigning to a temporary copy is because shared_ptr and most of the std library was designed before we had the ability to overload operator= based on the r/lvalue-ness of the left hand side.
...
Now, why is this the case? Actual constness is used by the compiler as an optimization hint.
{
const std::shared_ptr<int> bob = std::make_shared<int>();
}
in the above case, the compiler can know for certain that bob is non-empty at the end of the scope. Nothing can be done to bob that could make it empty and still leave you with defined behavior.
So the compiler can eliminate the branch at the end of the scope when destroying bob that checks if the pointer is null.
Similar optimizations could occur if you pass bob to an inline function that checks for bob's null state; the compiler can omit the check.
Suppose you pass bob to
void secret_code( std::shared_ptr<int> const& );
where the compiler cannot see into the implementation of secret_code. It can assume that secret code will not edit bob.
If it wasn't declared const, secret_code could legally do a const_cast<std::shared_ptr&> on the parameter and set it to null; but if the argument to secret_code is actually const this is undefined behavior. (Any code casting away const is responsible for guaranteeing that no actual modification of an actual const value occurs by doing so)
Without const on bob, the compiler could not guarantee:
{
const std::shared_ptr<int> bob = std::make_shared<int>();
secret_code(bob);
if (bob) {
std::cout << "guaranteed to run"
}
}
that the guaranteed to run string would be printed.
With const on bob, the compiler is free to elimiate the if check above.
...
Now, do not confuse my explanation asto why the standard states you cannot edit const stack variables with "if this doesn't happen there is no problem". The standard states you shall not do it; the consequences if you do it are unbounded and can grow with new versions of your compiler.
...
From comments:
For deserialize process, which is actually a type of constructor that deserialize object from file. C++ is nice, but it got its imperfections and sometimes its OK to search for less orthodox methods.
If it is a constructor, make it a constructor.
In C++17 a function returning a T has basically equal standing to a real constructor in many ways (due to guaranteed elision). In C++14, this isn't quite true (you also need a move constructor, and the compiler needs to elide it).
So a deserialization constructor for a type T in C++ needs to return a T, it cannot take a T by-reference and be a real constructor.
Composing this is a bit of a pain, but it can be done. Using the same code for serialization and deserialization is even more of a pain (I cannot off hand figure out how).

Related

When to rely on RVO vs move semantics in C++?

Say I have some expensive class X, and take this code:
X functor() {
X x;
//do stuff
return x;
}
int main() {
std::vector<X> vec;
vec.push_back(functor());
vec.push_back(std::move(functor()));
return 0;
}
Which push_back is more efficient? In the first case, won't the NRVO be activated and prevent the copy as the move does? Should I be relying on the NRVO instead of doing manual moving, since the NRVO is basically automatic moving?
Neither piece of code is more efficient; they do the exact same thing. And neither push_back expression involves elision of any kind. The only actual elision happened back in the return statement, which the push_back expressions don't interact with.
In both push_back expressions, the prvalue returned by the function will manifest a temporary. In the latter case, the temporary will be cast into an xvalue, but that doesn't represent any actual runtime changes. Both versions will call the same push_back overload: the one which takes an rvalue reference. And therefore in both cases the return value will be moved from.
#Nichol Bolas' answer is correct, as far as it goes.
It seems to me that you don't entirely understand what std::move does, or when it's intended to be used.
std::move is (at least primarily) used when you have a parameter or rvalue reference type, something like this: int foo(int &&param);. The problem is that even though param has to be initialized from an rvalue, and refers to an rvalue, param itself has a name (i.e., param) so it is not itself an rvalue. Because of that, the compiler can't/won't automatically recognize that param can be used as the source of a move operation. So, if you want to do a move out of param, you need to use std::move to tell the compiler that this is an rvalue, so the compiler can move from it instead of copying from it.
In your code: vec.push_back(std::move(functor()));, you're applying the std::move to the temporary value that holds the return from the function. Since this is a temporary rather than a named variable, the compiler can/will automatically recognize that it can be used as the source of a move, so std::move has no hope of accomplishing anything.
Although it doesn't really apply in this specific case, the other point to keep in mind for situations somewhat like this is using emplace_back instead of push_back. This can allow your code to avoid building and then copying/moving an object at all. Instead, it can create references to the parameters that you pass to the ctor to create the object, so that inside of emplace_back itself, you create an object (once) in the spot it's going to occupy in the target vector. For example:
struct foo {
int i;
double d;
foo(int i, double d) : i(i), d(d) {}
};
std::vector<foo> f;
int a = 123;
double b = 456.789;
f.push_back(foo(a, b));
f.emplace_back(a, b);
In this case, the push_back creates a temporary foo object, initialized from a and b . Since that's a temporary, the compiler recognizes that it can do a move from there into the spot it's going to occupy in the vector.
But the emplace_back avoids creating the temporary at all. Instead, it just passes references to a and b, and inside of emplace_back, when it's ready to create the object in the vector, it creates it, initializing it directly from a and b.
foo directly contains data, rather than containing a pointer to the data. That mean in this case, doing a move is likely to gain little or nothing compared to doing a copy, so even though push_back can accept an rvalue and do a move out of it into the target, it wont' do much good in this case--it'll end up copying the data anyway. But with emplace_back, we'll avoid that completely, and just initialize the object directly from the source data (a and b), so we'll copy them once--but only once.
NRVO implies that automatic storage described by x is identical to storage referenced by rvalue result of functor() call. std::move() in this case is late for the party and gets no credit.

Detecting unsafe const reference binding in C++

I've just spent quite a lot of time debugging an obscure memory corruption problem in one of my programs. It essentially boils down to a function which returns a structure by value being called in a way which passes it into an object constructor. Pseudocode follows.
extern SomeStructure someStructureGenerator();
class ObjectWhichUsesStructure {
ObjectWhichUsesStructure(const SomeStructure& ref): s(ref) {}
const SomeStructure& s;
}
ObjectWhichUsesStructure obj(someStructureGenerator());
My reasoning was: someStructureGenerator() is returning a temporary; this is being bound to a const reference, which means the compiler is extending the lifetime of the temporary to match the place of use; I'm using it to construct an object, so the temporary lifetime is being extended to match that of the object.
That last bit turns out not to be the case. Once the constructor exits, the compiler deletes the temporary and now obj contains a reference to hyperspace, with hilarious results when I try to use it. I need to explicitly bind the const reference to the scope, like this:
const auto& ref = someStructureGenerator();
ObjectWhichUsesStructure obj(ref);
That's not the bit I'm asking about.
What I'm asking about is this: my compiler is gcc 8, I build with -Wall, and it was perfectly happy to compile the code above --- cleanly, with no warnings. My program ran happily (but incorrectly) under valgrind, likewise with no warnings.
I have no idea how many other places in my code I'm using the same idiom. What compiler tooling will detect and flag these places so that I can fix my code, and alert me if I make the same mistake in the future?
First, reference binding does “extend lifetime” here—but only to the lifetime of the constructor parameter (which is no longer than that of the temporary materialized anyway). s(ref) isn’t binding an object (since ref is, well, already a reference), so no further extension occurs.
It’s therefore possible to perform the extension you expected via aggregate initialization:
struct ObjectWhichUsesStructure {
const SomeStructure &s;
};
ObjectWhichUsesStructure obj{someStructureGenerator()};
Here there is no constructor parameter (because there is no constructor at all!) and so only the one, desired binding occurs.
It’s worth seeing why the compiler doesn’t warn about this: even if a constructor does retain a reference to a temporary argument, there are legitimate situations where that works:
void useWrapper(ObjectWhichUsesStructure);
void f() {useWrapper(someStructureGenerator());}
Here the SomeStructure lives until the end of the statement, during which time useWrapper can make profitable use of the reference in the ObjectWhichUsesStructure.
At the expense of forbidding the valid use cases above, you can have the compiler trap the problematic case by providing a deleted constructor taking an rvalue reference:
struct ObjectWhichUsesStructure {
ObjectWhichUsesStructure(const SomeStructure& ref): s(ref) {}
ObjectWhichUsesStructure(SomeStructure&&)=delete;
const SomeStructure& s;
};
This might be worth doing temporarily as a diagnostic measure without having it be a permanent restriction.

passing as rvalue reference vs double-move value

Suppose I have the following class:
class foo {
std::unique_ptr<blah> ptr;
}
What's the difference between these two:
foo::foo(unique_ptr p)
: ptr(std::move(p))
{ }
and
foo::foo(unique_ptr&& p)
: ptr(std::move(p)
{ }
When called like
auto p = make_unique<blah>();
foo f(std::move(p));
Both compile, and I guess both must use unique_ptr's move constructors? I guess the first one it'll get moved twice, but the second one it'll only get moved once?
They do the same, which is moving the pointer twice (which translates to 2 casts).
The only practical difference is the place where the code would break if you were to pass a std::unique_ptr by value.
Having foo::foo(unique_ptr p) the compiler would complain about the copy constructor being deleted.
Having foo::foo(unique_ptr&& p) it would say that there is no matching function for the set of arguments provided.
std::unique_ptr is a non-copyable class, this means that you either pass a constant reference (readonly access) or move it (rvalue) to express/give ownership.
The second case (getting explicit rvalue) is the proper implementation, as your intent is passing the ownership and std::unique_ptr has deleted copy assignment/constructor.
In the first case, copy is prohibited and the arg is an rvalue (you moved it before passing) so the compiler uses the move constructor. Then the ownership belongs to the new argument and you move it again to the internal field passing the ownership again, not a very nice solution. (Note that this declaration isn't restrictive, it allows any value type. If std::unique_ptr would be copyable, copy contructor would be used here, hiding the actual problem).
In terms of resulting code, std::move is a simple cast to rvalue, so both do the same. In terms of correctness, second one must be used.

C++: Why is const_cast evil?

I keep hearing this statement, while I can't really find the reason why const_cast is evil.
In the following example:
template <typename T>
void OscillatorToFieldTransformer<T>::setOscillator(const SysOscillatorBase<T> &src)
{
oscillatorSrc = const_cast<SysOscillatorBase<T>*>(&src);
}
I'm using a reference, and by using const, I'm protecting my reference from being changed. On the other hand, if I don't use const_cast, the code won't compile. Why would const_cast be bad here?
The same applies to the following example:
template <typename T>
void SysSystemBase<T>::addOscillator(const SysOscillatorBase<T> &src)
{
bool alreadyThere = 0;
for(unsigned long i = 0; i < oscillators.size(); i++)
{
if(&src == oscillators[i])
{
alreadyThere = 1;
break;
}
}
if(!alreadyThere)
{
oscillators.push_back(const_cast<SysOscillatorBase<T>*>(&src));
}
}
Please provide me some examples, in which I can see how it's a bad idea/unprofessional to use a const_cast.
Thank you for any efforts :)
Because you're thwarting the purpose of const, which is to keep you from modifying the argument. So if you cast away the constness of something, it's pointless and bloating your code, and it lets you break promises that you made to the user of the function that you won't modify the argument.
In addition, using const_cast can cause undefined behaviour. Consider this code:
SysOscillatorBase<int> src;
const SysOscillatorBase<int> src2;
...
aFieldTransformer.setOscillator(src);
aFieldTransformer.setOscillator(src2);
In the first call, all is well. You can cast away the constness of an object that is not really const and modify it fine. However, in the second call, in setOscillator you are casting away the constness of a truly const object. If you ever happen to modify that object in there anywhere, you are causing undefined behaviour by modifying an object that really is const. Since you can't tell whether an object marked const is really const where it was declared, you should just never use const_cast unless you are sure you'll never ever mutate the object ever. And if you won't, what's the point?
In your example code, you're storing a non-const pointer to an object that might be const, which indicates you intend to mutate the object (else why not just store a pointer to const?). That might cause undefined behaviour.
Also, doing it that way lets people pass a temporary to your function:
blah.setOscillator(SysOscillatorBase<int>()); // compiles
And then you're storing a pointer to a temporary which will be invalid when the function returns1. You don't have this problem if you take a non-const reference.
On the other hand, if I don't use const_cast, the code won't compile.
Then change your code, don't add a cast to make it work. The compiler is not compiling it for a reason. Now that you know the reasons, you can make your vector hold pointers to const instead of casting a square hole into a round one to fit your peg.
So, all around, it would be better to just have your method accept a non-const reference instead, and using const_cast is almost never a good idea.
1 Actually when the expression in which the function was called ends.
by using const, I'm protecting my reference from being changed
References can't be changed, once initialized they always refer to the same object. A reference being const means the object it refers to cannot be changed. But const_cast undoes that assertion and allows the object to be changed after all.
On the other hand, if I don't use const_cast, the code won't compile.
This isn't a justification for anything. C++ refuses to compile code that may allow a const object to be changed because that is the meaning of const. Such a program would be incorrect. const_cast is a means of compiling incorrect programs — that is the problem.
For example, in your program, it looks like you have an object
std::vector< SysOscillatorBase<T> * > oscillators
Consider this:
Oscillator o; // Create this object and obtain ownership
addOscillator( o ); // cannot modify o because argument is const
// ... other code ...
oscillators.back()->setFrequency( 3000 ); // woops, changed someone else's osc.
Passing an object by const reference means not only that the called function can't change it, but that the function can't pass it to someone else who can change it. const_cast violates that.
The strength of C++ is that it provides tools to guarantee things about ownership and value semantics. When you disable those tools to make the program compile, it enables bugs. No good programmer finds that acceptable.
As a solution to this particular problem, it looks likely that the vector (or whatever container you're using) should store the objects by value, not pointer. Then addOscillator can accept a const reference and yet the stored objects are modifiable. Furthermore, the container then owns the objects and ensures they are safely deleted, with no work on your part.
The use of const_cast for any reason other than adapting to (old) libraries where the interfaces have non-const pointers/references but the implementations don't modify the arguments is wrong and dangerous.
The reason that it is wrong is because when your interface takes a reference or pointer to a constant object you are promising not to change the object. Other code might depend on you not modifying the object. Consider for example, a type that holds an expensive to copy member, and that together with that it holds some other invariants.
Consider a vector<double> and a precomputed average value, the *average is updated whenever a new element is added through the class interface as it is cheap to update then, and if it is requested often there is no need to recompute it from the data every time. Because the vector is expensive to copy, but read access might be needed the type could offer a cheap accessor that returns a std::vector<double> const & for user code to check values already in the container. Now, if user code casts away the const-ness of the reference and updates the vector, the invariant that the class holds the average is broken and the behavior of your program becomes incorrect.
It is also dangerous because you have no guarantee that the object that you are passed is actually modifiable or not. Consider a simple function that takes a C null terminated string and converts that to uppercase, simple enough:
void upper_case( char * p ) {
while (*p) {
*p = ::to_upper(*p);
++p;
}
}
Now lets assume that you decide to change the interface to take a const char*, and the implementation to remove the const. User code that worked with the older version will also work with the new version, but some code that would be flagged as an error in the old version will not be detected at compile time now. Consider that someone decided to do something as stupid as upper_case( typeid(int).name() ). Now the problem is that the result of typeid is not just a constant reference to a modifiable object, but rather a reference to a constant object. The compiler is free to store the type_info object in a read-only segment and the loader to load it in a read-only page of memory. Attempting to change it will crash your program.
Note that in both cases, you cannot know from the context of the const_cast whether extra invariants are maintained (case 1) or even if the object is actually constant (case 2).
On the opposite end, the reason for const_cast to exist was adapting to old C code that did not support the const keyword. For some time functions like strlen would take a char*, even though it is known and documented that the function will not modify the object. In that case it is safe to use const_cast to adapt the type, not to change the const-ness. Note that C has support for const for a very long time already, and const_cast has lesser proper uses.
The const_cast would be bad because it allows you to break the contract specified by the method, i.e. "I shall not modify src". The caller expects the method to stick to that.
It's at least problematic. You have to distinguish two constnesses:
constness of the instantiated variable
This may result in physical constness, the data being placed in a read-only segment
constness of the reference parameter / pointer
This is a logical constness, only enforced by the compiler
You are allowed to cast away the const only if it's not physically const, and you can't determine that from the parameter.
In addition, it's a "smell" that some parts of your code are const-correct, and others aren't. This is sometimes unavoidable.
In your first example, you assign a const reference to what I assume is a non-const pointer. This would allow you to modify the original object, which requires at least a const cast. To illustrate:
SysOscillatorBase<int> a;
const SysOscillatorBase<int> b;
obj.setOscillator(a); // ok, a --> const a --> casting away the const
obj.setOscilaltor(b); // NOT OK: casting away the const-ness of a const instance
Same applies to your second example.
, while I can't really find the reason why const_cast is evil.
It is not, when used responsibily and when you know what you're doing. (Or do you seriously copy-paste code for all those methods that differ only by their const modifier?)
However, the problem with const_cast is that it can trigger undefined behavior if you use it on variable that originally was const. I.e. if you declare const variable, then const_cast it and attempt to modify it. And undefined behavior is not a good thing.
Your example contains precisely this situation: possibly const variable converted into non-const. To avoid the problem store either const SysOscillatorBase<T>*(const pointer) or SysOscillatorBase<T> (copy) in your object list, or pass reference instead of const reference.
You are violating a coding contract. Marking a value as const is saying you can use this value but never change it. const_cast breaks this promise and can create unexpected behaviour .
In the examples you give, it seems your code is not quite right. oscillatorSrc should probably be a const pointer, although if you really do need to change the value then you should not pass it in as a const at all.
Basicly const promises you and the compiler that you will not change the value. The only time you should use when you use a C library function (where const didn't exist), that is known not to change the value.
bool compareThatShouldntChangeValue(const int* a, const int* b){
int * c = const_cast<int*>(a);
*c = 7;
return a == b;
}
int main(){
if(compareThatShouldntChangeValue(1, 7)){
doSomething();
}
}
You probably need to define you container as containing const objects
template <typename T> struct Foo {
typedef std::vector<SysOscillator<T> const *> ossilator_vector;
}
Foo::ossilator_vector<int> oscillators;
// This will compile
SysOscillator<int> const * x = new SysOscillator<int>();
oscillators.push_back(x);
// This will not
SysOscillator<int> * x = new SysOscillator<int>();
oscillators.push_back(x);
That being said if you have no control over the typedef for the container maybe it
is ok to const_cast at the interface between your code and the library.

Construct object with itself as reference?

I just realised that this program compiles and runs (gcc version 4.4.5 / Ubuntu):
#include <iostream>
using namespace std;
class Test
{
public:
// copyconstructor
Test(const Test& other);
};
Test::Test(const Test& other)
{
if (this == &other)
cout << "copying myself" << endl;
else
cout << "copying something else" << endl;
}
int main(int argv, char** argc)
{
Test a(a); // compiles, runs and prints "copying myself"
Test *b = new Test(*b); // compiles, runs and prints "copying something else"
}
I wonder why on earth this even compiles. I assume that (just as in Java) arguments are evaluated before the method / constructor is called, so I suspect that this case must be covered by some "special case" in the language specification?
Questions:
Could someone explain this (preferably by referring to the specification)?
What is the rationale for allowing this?
Is it standard C++ or is it gcc-specific?
EDIT 1: I just realised that I can even write int i = i;
EDIT 2: Even with -Wall and -pedantic the compiler doesn't complain about Test a(a);.
EDIT 3: If I add a method
Test method(Test& t)
{
cout << "in some" << endl;
return t;
}
I can even do Test a(method(a)); without any warnings.
The reason this "is allowed" is because the rules say an identifiers scope starts immediately after the identifier. In the case
int i = i;
the RHS i is "after" the LHS i so i is in scope. This is not always bad:
void *p = (void*)&p; // p contains its own address
because a variable can be addressed without its value being used. In the case of the OP's copy constructor no error can be given easily, since binding a reference to a variable does not require the variable to be initialised: it is equivalent to taking the address of a variable. A legitimate constructor could be:
struct List { List *next; List(List &n) { next = &n; } };
where you see the argument is merely addressed, its value isn't used. In this case a self-reference could actually make sense: the tail of a list is given by a self-reference. Indeed, if you change the type of "next" to a reference, there's little choice since you can't easily use NULL as you might for a pointer.
As usual, the question is backwards. The question is not why an initialisation of a variable can refer to itself, the question is why it can't refer forward. [In Felix, this is possible]. In particular, for types as opposed to variables, the lack of ability to forward reference is extremely broken, since it prevents recursive types being defined other than by using incomplete types, which is enough in C, but not in C++ due to the existence of templates.
I have no idea how this relates to the specification, but this is how I see it:
When you do Test a(a); it allocates space for a on the stack. Therefore the location of a in memory is known to the compiler at the start of main. When the constructor is called (the memory is of course allocated before that), the correct this pointer is passed to it because it's known.
When you do Test *b = new Test(*b);, you need to think of it as two steps. First the object is allocated and constructed, and then the pointer to it is assigned to b. The reason you get the message you get is that you're essentially passing in an uninitialized pointer to the constructor, and the comparing it with the actual this pointer of the object (which will eventually get assigned to b, but not before the constructor exits).
The second one where you use new is actually easier to understand; what you're invoking there is exactly the same as:
Test *b;
b = new Test(*b);
and you're actually performing an invalid dereference. Try to add a << &other << to your cout lines in the constructor, and make that
Test *b = (Test *)0xFOOD1E44BADD1E5;
to see that you're passing through whatever value a pointer on the stack has been given. If not explicitly initialized, that's undefined. But even if you don't initialize it with some sort of (in)sane default, it'll be different from the return value of new, as you found out.
For the first, think of it as an in-place new. Test a is a local variable not a pointer, it lives on the stack and therefore its memory location is always well defined - this is very much unlike a pointer, Test *b which, unless explicitly initialized to some valid location, will be dangling.
If you write your first instantiation like:
Test a(*(&a));
it becomes clearer what you're invoking there.
I don't know a way to make the compiler disallow (or even warn) about this sort of self-initialization-from-nowhere through the copy constructor.
The first case is (perhaps) covered by 3.8/6:
before the lifetime of an object has
started but after the storage which
the object will occupy has been
allocated or, after the lifetime of an
object has ended and before the
storage which the object occupied is
reused or released, any lvalue which
refers to the original object may be
used but only in limited ways. Such an
lvalue refers to allocated storage
(3.7.3.2), and using the properties of
the lvalue which do not depend on its
value is well-defined.
Since all you're using of a (and other, which is bound to a) before the start of its lifetime is the address, I think you're good: read the rest of that paragraph for the detailed rules.
Beware though that 8.3.2/4 says, "A reference shall be initialized to refer to a valid object or function." There is some question (as a defect report on the standard) what "valid" means in this context, so possibly you can't bind the parameter other to the unconstructed (and hence, "invalid"?) a.
So, I'm uncertain what the standard actually says here - I can use an lvalue, but not bind it to a reference, perhaps, in which case a isn't good, while passing a pointer to a would be OK as long as it's only used in the ways permitted by 3.8/5.
In the case of b, you're using the value before it's initialized (because you dereference it, and also because even if you got that far, &other would be the value of b). This clearly is not good.
As ever in C++, it compiles because it's not a breach of language constraints, and the standard doesn't explicitly require a diagnostic. Imagine the contortions the spec would have to go through in order to mandate a diagnostic when an object is invalidly used in its own initialization, and imagine the data flow analysis that a compiler might have to do to identify complex cases (it may not even be possible at compile time, if the pointer is smuggled through an externally-defined function). Easier to leave it as undefined behavior, unless anyone has any really good suggestions for new spec language ;-)
If you crank your warning levels up, your compiler will probably warn you about using uninitialized stuff. UB doesn't require a diagnostic, many things that are "obviously" wrong may compile.
I don't know the spec reference, but I do know that accessing an uninitialized pointer always results in undefined behaviour.
When I compile your code in Visual C++ I get:
test.cpp(20): warning C4700:
uninitialized local variable 'b' used